feat(mobile): bring inbox analytics to parity with desktop#2411
Conversation
Mirror desktop's inbox event names + property shapes on the mobile app so both clients are comparable in PostHog dashboards. Events wired: - `Inbox viewed` — fired once per inbox-tab focus, with priority/actionability breakdown counts (per #2295). - `Inbox report opened` / `closed` / `scrolled` — fired from the report detail screen via a new `useInboxEngagementTracker` hook, with priority + actionability snapshotted at open time. - `Inbox report action` — fired for: - Dismiss / snooze via the dismiss sheet (with `dismissal_reason` and `dismissal_note`, truncated to 1000 chars per #2287). - "Start task" on the detail screen → `create_pr`, surface `detail_pane`. - Tinder swipe-right (accept) → `create_pr`, surface `list_row`. - Tinder swipe-left (dismiss) → `dismiss`, surface `list_row`. - Expanding the Signals list on the detail screen → `expand_signal`. For #2369 (tagging discuss-launched task events with `signal_report_id`): the task detail screen registers a `signal_report_id` PostHog super-property for the duration of the screen when the task carries one, matching the desktop super-property behaviour. #2380 (`Signal source connected`) is intentionally skipped: there is no signal-source connect / data-source-setup flow in the mobile app. We do not fire any "Task created" event on mobile yet, so the desktop-side `Task created` tagging in #2369 also doesn't apply here — only the super-property half does, and that's what we wired. Architecture: analytics types and the `useAnalytics()` / `useActiveTaskAnalyticsContext()` hooks live in `apps/mobile/src/lib/analytics.ts`. Event names and property shapes are literal mirrors of `apps/code/src/shared/types/analytics.ts` — no shared package extraction since the desktop types live inside `apps/code`, not in a shared workspace. Tests: - `apps/mobile/src/lib/analytics.test.ts` — `computeReportAgeHours` and the `useActiveTaskAnalyticsContext` super-property lifecycle. - `apps/mobile/src/features/inbox/utils.test.ts` — `buildInboxViewedProperties` (priority/actionability counts, has_active_filters detection). - `apps/mobile/src/features/inbox/hooks/useInboxEngagementTracker.test.ts` — OPENED/CLOSED/SCROLLED lifecycle, scroll-once semantics, and signalAction inheritance vs. overrides. `pnpm --filter @posthog/mobile test` → 119 passed (14 new). `pnpm lint` clean. No new TypeScript errors. Generated-By: PostHog Code Task-Id: 2e652fec-af01-4476-b6cf-ab9b4de015db
|
Two fixes from the Greptile bot review: 1. **`Inbox viewed` was silently skipped on re-focus when data was cached** (P1). The `useFocusEffect` cleanup reset `viewedFiredRef` to `false`, but the firing `useEffect` only re-runs when its deps change — and on a re-focus with cached data, none of the deps had changed, so the event never fired again. Switched to a `focusVersion` state counter that bumps on every focus (so it appears in the effect's dep list) and changed the guard ref to remember which focus-version we last fired for, so we still only fire once per focus. 2. **`expand_signal` analytics fired inside a `setState` updater** (P2). React Strict Mode double-invokes updaters in development, which would double-fire the event. Read the next value outside the updater, fire the analytics, then call `setSignalsExpanded(next)`. No behaviour change beyond the bug fixes. `pnpm --filter @posthog/mobile lint` clean, all 119 tests still passing, no new TypeScript errors. Generated-By: PostHog Code Task-Id: 2e652fec-af01-4476-b6cf-ab9b4de015db
…ENED spikes Greptile P1 (Comments Outside Diff): `useInboxEngagementTracker`'s lifecycle `useEffect` had `report`, `rank`, `listSize`, `openMethod`, and `previousReportId` in its dep list. When the inbox list refetched in the background, any change to those values (e.g. the same report shifting rank, or the report's priority/actionability shape changing) would fire a spurious `INBOX_REPORT_CLOSED` with near-zero `time_spent_ms` followed immediately by a new `INBOX_REPORT_OPENED`. Snapshot those inputs through refs so only `reportId` gates open/close. The OPENED event still records rank / listSize / open method as-of mount, but a same-report shape change can no longer churn the lifecycle. Added a regression test asserting that rerendering with a new rank + listSize + report shape (same `id`) does not produce any additional OPENED or CLOSED events. Generated-By: PostHog Code Task-Id: 2e652fec-af01-4476-b6cf-ab9b4de015db
There was a problem hiding this comment.
The PR exceeds the automated review size ceiling (1273 lines, 14 files). It introduces new analytics infrastructure (a new analytics module) and a new engagement tracker hook, which warrants human review to validate the event naming conventions, correctness of tracking logic, and absence of data leakage.
k11kirky
left a comment
There was a problem hiding this comment.
would prefer we run a /simplify skill on these as part of the prompt <3 very verbose comments
Resolve rename conflict in apps/mobile/src/app/inbox: main renamed [id].tsx to the catch-all [...id].tsx and added the Discuss feature; this branch added inbox engagement analytics. Combined both import sets. Generated-By: PostHog Code Task-Id: 3c668694-a211-41db-8646-068c350d22bb
Generated-By: PostHog Code Task-Id: 3c668694-a211-41db-8646-068c350d22bb
Temporary scaffold: makes the catch-all route file byte-identical to main so the GitHub merge API (which lacks rename detection) can merge main in without an add/add conflict. The PR's inbox analytics are re-applied in the following commit. Generated-By: PostHog Code Task-Id: 3c668694-a211-41db-8646-068c350d22bb
This script belongs to main and was accidentally baked into the branch by an earlier merge commit (at the wrong file mode). Removing it so merging main back in restores it cleanly at its correct executable mode and keeps it out of this PR's diff. Generated-By: PostHog Code Task-Id: 3c668694-a211-41db-8646-068c350d22bb
Restores this PR's inbox-detail analytics (engagement tracker, signal-expand/start-task/dismiss tracking) on top of main's catch-all route + Discuss feature, after the temporary scaffold that reset the file to main's version to let the merge through cleanly. Generated-By: PostHog Code Task-Id: 3c668694-a211-41db-8646-068c350d22bb
Summary
Brings mobile-app inbox analytics up to parity with the desktop app so events fired from both clients land in the same PostHog buckets and can be funnelled together.
Mirrors the events introduced in desktop PRs #2228, #2287, #2295, and #2369.
Events wired on mobile
Inbox viewed— fired once per inbox-tab focus when reports settle. Carries the priority (priority_p{0..4}_count,priority_unknown_count) and actionability (actionability_immediately_actionable_count, etc.) breakdowns from feat(code): add priority + actionability to all inbox events #2295, plusreport_count/total_count/ready_count/source_product_filter/status_filter_count/has_active_filters/is_empty/is_gated_due_to_scale: false.Inbox report opened— fired from the/inbox/[id]detail screen on mount, withrank+list_sizesnapshotted from the list view,open_method(currently"click"by default — mobile has no Cmd/Shift modifiers),previous_report_idchained across opens via the inbox store, and thepriority+actionabilitysnapshot from feat(code): add priority + actionability to all inbox events #2295.Inbox report closed— fired when the detail screen unmounts (close_method: "deselected"), withtime_spent_ms,scrolled, and snapshotted priority/actionability.Inbox report scrolled— fired once per open on the first scroll inside the detail pane, withtime_since_open_ms.Inbox report action— fired for:DismissReportSheet(withdismissal_reasonanddismissal_notetruncated to 1000 chars per feat(inbox): capture dismissal_note in dismiss analytics event #2287). Snooze is detected the same way as desktop, via thesnoozesInsteadOfDismissflag on the reason option.create_pr, surfacedetail_pane.create_pr, surfacelist_row.dismiss, surfacelist_row.expand_signal.For #2369 (tag discuss-launched task events with
signal_report_id): the task detail screen registers asignal_report_idPostHog super-property whenever the loaded task has one, and clears it on unmount. Every subsequent event fired from that task screen carries the signal report id, matching the desktopsetActiveTaskAnalyticsContextbehaviour.Events intentionally skipped
Signal source connected— mobile has no signal-source toggle orDataSourceSetupwizard surface. There is no place inapps/mobileto fire this from without inventing a flow.Task createdtagging — mobile does not currently fire aTask createdanalytics event at all, so there is no event to add thesignal_report_idproperty to. Only the super-property half of feat(analytics): tag discuss-launched task events with signal_report_id #2369 ports cleanly to mobile, and that's what's implemented here.Implementation notes
apps/mobile/src/lib/analytics.tshousesANALYTICS_EVENTS, the typed property interfaces,useAnalytics(),useActiveTaskAnalyticsContext(), andcomputeReportAgeHours(). Event names + shapes are literal mirrors ofapps/code/src/shared/types/analytics.ts— kept in lockstep manually since the desktop types live insideapps/code(no shared workspace package to import from).useInboxEngagementTracker(mobile-adapted port of the desktop hook of the same name) owns the OPENED/CLOSED/SCROLLED lifecycle keyed on the open report.inboxStoregainedlastVisibleReportIds+previousOpenedReportIdso the detail screen can recordrank/list_size/previous_report_idon OPENED without prop-drilling.DismissReportSheetnow passes the dismissal{ reason, note }back viaonDismissedso the parent can route it through analytics — keeping the sheet stateless about which surface launched it (toolbar vs. detail).Test plan
pnpm --filter @posthog/mobile test— 119 passed (14 new acrosslib/analytics.test.ts,features/inbox/utils.test.ts,features/inbox/hooks/useInboxEngagementTracker.test.ts).pnpm --filter @posthog/mobile lint— clean.npx tsc --noEmitcount unchanged at 47 pre-existing errors).Inbox viewedin PostHog activity feed with the new breakdown counts.Inbox report openedwithrank/list_size/previous_report_id/priority/actionability. Scroll inside → confirm oneInbox report scrolled. Tap back → confirmInbox report closedwith realistictime_spent_msandscrolled: true.Inbox report actionwithaction_type: "dismiss",dismissal_reason,dismissal_note(truncated at 1000 chars if longer). Empty/whitespace-only note → nodismissal_note. Reasonalready_fixed→action_type: "snooze"and no dismissal_reason/note attached.Inbox report actionwithaction_type: "dismiss",surface: "list_row". Tinder swipe-right (accept) →Inbox report actionwithaction_type: "create_pr",surface: "list_row".Inbox report actionwithaction_type: "create_pr",surface: "detail_pane". Subsequent task screen events carry thesignal_report_idsuper-property. Navigating back to the inbox clears the super-property.