PRODUCT / FUNNEL (GA4): if analytics_* exists, compute:
a) ACTIVATION: first_open → first_sound_add → first_sound_play → first_share. % conversion
between steps.
b) CREATION CHANNELS: sound_add broken down by param source — above all import vs record
(import a file vs record in-app), plus others. How users create Bomps.
c) IMPORT HUB FUNNEL: import_hub_opened → (import_hub_import_selected | import_hub_record_selected)
→ sound_add with source=import/record. Conversion and drop-off per stage. Treat
source=onboarding_finish as a separate cohort (don't put it in the denominator of proactive intent
fab/my_sounds_empty_state).
d) RECORDER FUNNEL (new feature, ADR 0019): import_hub_record_selected → record_permission_result
(mic GRANT RATE = granted true/total) → recording_completed → sound_add with source=record.
Mark where people fall off. Draft recovery: recording_draft_banner_shown → recording_draft_resumed
vs recording_draft_discarded (resume rate).
e) ONBOARDING TOUR (3 steps, step_key ∈ import/organize/bompear): onboarding_opened →
onboarding_step_viewed → onboarding_completed vs onboarding_dismissed. Completion rate =
completed/opened. Drop-off step = last step_key of the dismissed. NOTE: onboarding_step_viewed is
re-emitted on back/re-entry → use COUNT(DISTINCT user_pseudo_id) per step_key, not COUNT(*).
Build the funnel over step_key (not over the step index). Separate by source (import_hub vs
my_sounds_empty_state).
f) WELCOME STICKER (passive onboarding): welcome_sticker_shown → welcome_sticker_play →
welcome_sticker_completed vs welcome_sticker_dismissed.
g) FRICTION: sound_add_abandoned_after_error (+ param reason); rate of sound_add with
name_hit_limit=true; duplicate_name_hint_shown vs duplicate_name_hint_play.
h) ENGAGEMENT: sound_play and sound_add totals and per active user; pin_toggle, visibility_toggle;
milestones (event_name LIKE 'milestone_sounds_%').
i) VIRALITY: share and first_share (sharers / actives rate).
j) FEATURE ADOPTION: Collections (collection_create by scope=public/private and by source;
collection_audio_toggle, collection_filter_apply); Vault (vault_unlock rate granted=true;
vault_unprotected_warning_shown; vault_search_unlock_cta_shown).
k) UNMET DEMAND: search_zero_results (+ query_length).
l) MONETIZATION: about_gratitude_cafecito_open + about_gratitude_kofi_open.
m) NAVIGATION: screen_view broken down by screen. Read the name from event_params
key firebase_screen — the export uses that key, not screen_name (Firebase renames
the SDK's SCREEN_NAME param). Values (CanonicalScreenName.kt): my_sounds,
explore_sounds, about, search_sound, add_sound, edit_sound, vault, vault_listen,
vault_unlock, collection_create, manage_collections, onboarding, bring_guide, record_sound.
Available user properties (to segment if useful): current_sounds, current_pinned,
current_public_colls, current_private_colls, current_audios_in_colls, current_public_default,
current_public_custom, current_vault_default, current_vault_custom, lifetime_shares,
lifetime_plays, lifetime_coll_creates, lifetime_coll_deletes, lifetime_coll_renames,
lifetime_coll_assigns, lifetime_vault_unlocks.
GA4 windowing (use this WHERE in every GA4 query — it fixes three traps): filter precisely on
event_timestamp (INT64 microseconds in the GA4 export — wrap in TIMESTAMP_MICROS to compare with a
TIMESTAMP, else BigQuery raises a type error) for an exact N×24h window (so it matches the sessions
axis, not 8 days), and prune
tables with a regex that matches BOTH finalized events_YYYYMMDD AND events_intraday_YYYYMMDD
(otherwise the most recent 1-3 days — which lag finalization, and the intraday table — are silently
dropped right after a release):
WHERE TIMESTAMP_MICROS(event_timestamp) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
AND REGEXP_EXTRACT(_TABLE_SUFFIX, r'[0-9]{8}$') >= FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 8 DAY))
(For 7d-vs-prev-7d, widen both bounds to 14 days and split on event_timestamp.)
GA4 reference query (event counts, windowed per the rule above): SELECT event_name, COUNT(*) n, COUNT(DISTINCT user_pseudo_id) usuarios FROM bomp-prod.analytics_XXX.events_* WHERE TIMESTAMP_MICROS(event_timestamp) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(),INTERVAL 7 DAY) AND REGEXP_EXTRACT(_TABLE_SUFFIX, r'[0-9]{8}$') >= FORMAT_DATE('%Y%m%d',DATE_SUB(CURRENT_DATE(),INTERVAL 8 DAY)) GROUP BY event_name ORDER BY n DESC.
Prioritize funnels with signal; if a funnel has 0 events, summarize it in one line, don't break it down.
0-in-window ≠ broken instrumentation — but cut by version before deciding. A 0 has two very
different causes (a quiet week vs. a tracker that died); recency alone can't tell them apart, because a
fresh regression looks exactly like a quiet week if the event fired fine on the previous version. Work
the checks in this order:
- Recency — get the event's last-seen date / 30–90 day count. Never fired at all → instrumentation
gap, stop here.
- Version — if it fired historically, cut it by
app_info.version. Still emitting on the version
that dominates the window → instrumentation is healthy and the 0 is real usage; report it as such,
never as a "possible gap". Alive on older versions and 0 on the current one → suspect a regression
introduced by that release and say so, even when nothing in the release obviously "touched" the
event — an R8 rule, a nav refactor, or a screen that quietly stopped calling the tracker all break it
without naming it.
- Corroborate before crying wolf — a 0 on a new version means nothing if barely anyone is on it, or
if the whole surface is idle. Require enough users on that version (same min-N guard as the
QUALITY/PERFORMANCE cuts) and check a sibling event on the same screen: siblings alive + this one
dead = instrumentation; the whole screen dead = real usage.
(Lesson: sound_add=0 in-window, last-seen 3 weeks ago, and the current version still emits it elsewhere
→ quiet week, not a gap. Same 0 with the event alive on the previous version, dead on the new one, while
recording_completed keeps firing there → regression, not a quiet week.)
Funnel coherence (read steps together, not in isolation). When an upstream step has signal and its
downstream step is 0, that is a drop-off to surface — not a contradiction and not a gap. Never frame
the same behavior as "activity happened" in one place and "no activity / maybe not instrumented" in
another. Canonical: recording_completed > 0 with sound_add (source=record) = 0 → users record but
don't save; the finding is the abandonment at the save step, not "no creations recorded".