screen_back_pressed, pm_payment_*) and the once-per-session dedupes are confirmed harmless because the insights that use those events read unique users (dau) or funnel entry, which the “keep first occurrence” rule preserves. The genuine exposure is two analytics findings (one count metric, one funnel) and three code-correctness bugs (concurrency / state-reset).
| ID | Area | Issue | Severity | Status |
|---|---|---|---|---|
| A1 | Analytics | “Total Search Events (Estimated)” undercounts | medium | Confirmed |
| A2 | Analytics | “Fork C” ordered funnel breaks on event timing | high | Confirmed |
| A3 | Analytics | Suffix dedupe rule is a blind match | low | Design risk |
| A4 | Analytics | Aggregation loses whole visit on hard-kill | medium | Design risk |
| B3 | Code | Dedupe state not reset on login/logout | high | Verify→fix |
| B1 | Code | Non-atomic read-modify-write race ($set) | high | Verify→fix |
| B2 | Code | Attribution dropped on flush race | high | Verify→fix |
| B8 | Code | Silent AsyncStorage failures | medium | Verify→fix |
| B5 | Code | Replay sampling per JS-load, not per session | low | Likely by design |
| C | Incidental | ud_-prefixed event names not in live taxonomy | low | Pre-existing |
Insight mz2whEOB is a count-based HogQL metric (sum(CASE…)) that includes name_search_performed, ud_interaction (action=owner_carousel_tapped), and ud_live_search_started. All three are now deduped once per session, so the count drops artificially post-culling. This is the only insight where dedupe changes a real number — everything else is dau.
Options: exclude these events from the gate · rewrite the insight to reconstruct volume · accept and annotate the metric.
Funnel IMbvIzad “Fork C: owner_search → naksha → property action” is an ordered funnel. Steps 2→3→4 are os_mode_entered → os_card_tapped → os_view_naksha — all now aggregated and flushed together on screen blur/background with near-identical exit-time timestamps. Blur ≈ the moment the user navigates to step 5 (ps_action).
Collapsing three sequential steps into one instant, and moving them to screen-exit time, breaks ordered-funnel timestamp monotonicity and can invert “step 4 before step 5” → the funnel under-counts conversion. Same risk class: 1EMel6Ep “Bookmark Journey” (uses ud_card_tapped).
Options: keep a lightweight per-tap event for funnel-critical steps and emit the aggregate under a new *_summary name · or adjust the funnels to tolerate the batched timing.
The gate dedupes any event ending in _screen_viewed / _nudge_shown / _nudge_dismissed. Any future event with those suffixes is silently capped once-per-session with no code change and no review. Recommend converting the suffix set to an explicit allow-list of event names.
polygon_tapped_summary and the card-tap family only flush on nav-blur + AppState background/inactive. A swipe-kill or JS crash mid-visit loses the entire visit’s taps. The previous code flushed every 5 taps, so partial data survived. Quantify accepted loss or add a flush-on-terminate safeguard.
seenThisSession (src/services/analyticsGate.js) resets only on cold start / 30-min background. setAnalyticsAuthState does not reset it. An event fired both pre- and post-login within one session (e.g. a *_screen_viewed) is dropped the second time → breaks signup funnels that expect the post-login occurrence.
Fix: export resetAnalyticsSession(); call it from setAnalyticsAuthState on auth transition. Add a regression test.
diffAgainstLastSent (src/services/posthog.js:222) does getItem → compute → setItem non-atomically, and the setItem is fire-and-forget. Two concurrent $set calls (e.g. deep-link + notification UTM sync) read the same baseline; the second write clobbers the first → a changed property is recorded as “already sent” and suppressed on the next session.
Fix: serialize person-prop writes via an in-memory promise chain (mutex); await the write. Add a concurrency test.
flushPendingAttribution (src/services/posthog.js:277) reads the queue, then removeItems at line 298. Anything queued in that window (e.g. a deep link arriving during login flush) is deleted unsent.
Fix: claim-and-clear (read + clear atomically at start; re-queue anything that races in). Add a test.
Storage errors in the diff/queue paths are swallowed with .catch(() => {}) and no Sentry breadcrumb. Fail-open is acceptable for $set (re-send), but queued-attribution loss should be surfaced.
replaySampledIn = Math.random() < 0.12 is drawn once at module load (App.js). The 30-min background rotation starts a new PostHog session while this stays fixed, so sampling is per-JS-process, not per-SDK-session. The code comment says this is intentional; document it and only change if product wants true per-session sampling.
Confirmed non-bugs (do not “fix”): disabled: __DEV__ making sampling dead-code in dev (correct); the setAnalyticsAuthState dependency array (correct — the gap it exposes is B3).
The event constants use ud_ prefixes (ud_live_search_started, ud_live_search_results_shown, ud_pagination_load_more), but the live taxonomy only has the un-prefixed names (live_search_started, …). Insights built on the old names are already missing new data from a prior rename. Flag to the analytics owner separately.
screen_back_pressed drop — 38,769 events / 10,924 users (30d), referenced by zero insights and zero experiments.*_screen_viewed / *_nudge_* / vm_action(my_location|search_opened) dedupe — every consuming insight uses math: "dau" or funnel first-step (verified: HMT0sf9w, 7zbmZH9D, 5002aqBN, 7F5xbufo, WAU aOmncKAG, Install→Signup funnels).pm_payment_* removal — all four referenced by zero insights and zero experiments.*_summary name vs tolerate batching.