Canonical client analytics contract. Emit only listed event_name values.
Companion CSV: docs/analytics/event_glossary.csv.
Coverage inventory: docs/analytics/glossary_emission_coverage.csv.
Architecture: ARCHITECTURE.md.
| Glossary name | Production source of truth | App action |
|---|---|---|
app_open |
SDK Application Opened | Do not capture("app_open"). Keep captureApplicationLifecycleEvents = true. |
app_background |
SDK Application Backgrounded | Do not emit custom background. |
| Install volume (DNI/WNI/MNI / Weekly Pulse) | SDK Application Installed | Keep Weekly Pulse on Application Installed / Application Opened. |
install_attributed |
Person $set_once install_channel |
Attribution only — never a second install-count event. |
deep_link_opened |
Custom glossary emit | SDK captureDeepLinks = false. |
Launch enrichment (first_seen_at, onboarding_status=pending) goes on person properties, not a second open event.
home_hero_resume, home_hero_jump_back_in, home_hero_new_episodes, home_hero_spotlight, home_mixtape, home_adaptive_*, home_because_you_like, home_discover_grid, home_video_spotlight, home_recommendations, briefing, explore_for_you, explore_trending, explore_category, explore_search_shows, explore_search_episodes, learn, podcast_detail, episode_detail, queue, downloads, history, liked, notification, deep_link, share, install_referrer, android_auto, android_auto_continue, android_auto_queue, android_auto_new_episodes, android_auto_mixtape, android_auto_downloads, android_auto_liked, android_auto_history, android_auto_discover, android_auto_voice, android_auto_play_all, mini_player, session_restore, player_up_next, smart_queue, onboarding_suggestion, unknown
Aliases (normalized in app; do not add as new enum values): learn_history → learn. Brief audio play tags briefing on playback_* (interactions remain on daily_briefing_action).
home, explore, learn, library_hub, library_subscriptions, library_liked, library_downloads, library_history, podcast_detail, episode_detail, player_full, player_mini, queue_sheet, settings, onboarding, briefing, android_auto, system_notification, share_sheet
hero_card_style / home_hero_card_style: block, cover, tide, duotone, halo, marquee (the names in Settings; stored keys standard and classic are reported as block and cover). hero_colors / home_hero_colors: dark, artwork, theme (App theme, the accent color), or none for Block, which takes no artwork colors. Marquee's Full color is the stored Dark choice, so it reports dark. changed: style or colors. home_hero_style_selected is skipped when a pick leaves the look unchanged. Tablets show Marquee as Cover; the event still reports the picked style.
section on recommendation_section_toggled: brief (The Brief), picked_for_you (Home's Picked for you and Explore's For you tab), because_you_like, time_of_day_picks (the greeting and its curated rows) and more_like_this (Try these episodes on episode pages). source: settings (Settings › Library › LeaveMeAlone) or brief_card (the Brief card's Always hide, which is the same choice as turning The Brief off). sections_off: how many of the five are off after the change. A section that is off is hidden and makes no requests.
podcast, episode, briefing, mixtape, announcement, unknown
- playback_mode:
stream|offline - client_surface:
phone|android_auto - install_channel:
play_store|apk_github|share_referrer|unknown - search_mode:
show_keyword|episode_semantic - mix_mode:
daily|offline - pause_reason:
user_voluntary|interruption|error|sleep_timer|app_kill|auto_pause - subscription_count_bucket:
0|1_3|4_10|10_plus - daypart:
morning|afternoon|evening|late_night
| event_name | meaning | required_properties | optional_properties | pii_risk |
|---|---|---|---|---|
app_open |
DAU volume via SDK Application Opened (glossary alias; not custom-emitted) | — (SDK) | person props for enrichment | none |
app_background |
Session end via SDK Application Backgrounded (not custom-emitted) | — (SDK) | — | none |
install_attributed |
Channel attribution only ($set_once install_channel); volume is Application Installed |
install_channel on person | referrer_raw; utm_source; share_token | none |
deep_link_opened |
Inbound deep/share link handled | link_scheme:string; is_first_open:bool; cold_start:bool | link_host:string; content_type:enum; podcast_id:string; episode_id:string | none |
onboarding_started |
User entered onboarding | entry_point:string | is_reentry:bool | none |
onboarding_flow_selected |
User chose AI/genre/search/import | flow_type:string; entry_point:string | — | none |
onboarding_step_viewed |
A discrete onboarding step became visible | step_name:string; flow_type:string | step_index:int | none |
onboarding_abandoned |
User left before complete | last_step:string; flow_type:string | time_spent_seconds:int; subscribed_count:int | none |
onboarding_completed |
User finished an onboarding path | method:string; total_subscribed_count:int | screen:string; subscribed_podcasts_list:list; total_onboarding_time_seconds:float; entry_point:string; favorite_genres:list; did_scroll_suggestions:bool; did_switch_from_ai:bool; import_type:string; searches_performed:int | none |
onboarding_ai_turn_submitted |
User sent a message in onboarding AI chat | turn_number:int; user_input_text:string; has_custom_input:bool | selected_options:list; time_spent_seconds:float | logged_by_policy |
onboarding_ai_response_received |
Assistant reply returned in onboarding AI | turn_number:int; options_count:int | options_list:list; duration_seconds:float; detected_intent:string; assistant_message:string | none |
onboarding_ai_search_redirect |
AI suggested leaving chat for search | turn_number:int | suggested_query:string | logged_by_policy |
onboarding_ai_synthesis_completed |
AI produced podcast suggestions | rows_count:int; podcasts_count:int | duration_seconds:float | none |
onboarding_ai_synthesis_failed |
AI suggestion generation failed | — | error_message:string | none |
onboarding_search_performed |
Search during onboarding | search_query:string; results_count:int | query_length:int | logged_by_policy |
onboarding_search_podcast_subscribed |
Subscribed from onboarding search result | podcast_id:string; total_subscribed_count:int | podcast_name:string | none |
onboarding_import_sheet_opened |
OPML/import sheet shown | — | entry_point:string | none |
onboarding_import_failed |
OPML/library import failed | import_type:string | error_message:string | none |
onboarding_manual_step_completed |
Genre/manual onboarding step finished | step_name:string; selections_count:int | selections_list:list; time_spent_seconds:float | none |
playback_started |
Audio playback started | episode_id:string; podcast_id:string; entry_point:enum; is_resume:bool; playback_mode:enum; client_surface:enum; speed:float; is_subscribed:bool | position_seconds:float; duration_seconds:float | none |
playback_heartbeat |
Periodic progress while playing | episode_id:string; podcast_id:string; entry_point:enum; position_seconds:float; playback_mode:enum; client_surface:enum | duration_seconds:float; percent_complete:float; milestone:int; speed:float; is_subscribed:bool | none |
playback_paused |
Playback paused | episode_id:string; podcast_id:string; entry_point:enum; position_seconds:float; listened_delta_seconds:float; pause_reason:enum; playback_mode:enum; client_surface:enum | duration_seconds:float; percent_complete:float; is_subscribed:bool | none |
playback_completed |
Episode reached completion | episode_id:string; podcast_id:string; entry_point:enum; listened_delta_seconds:float; playback_mode:enum; client_surface:enum | duration_seconds:float; is_subscribed:bool; speed:float | none |
playback_error |
Playback failed | error_type:string; entry_point:enum | episode_id:string; podcast_id:string; error_message:string; playback_mode:enum; client_surface:enum | none |
playback_buffering |
Buffering stall observed | entry_point:enum | episode_id:string; podcast_id:string; buffer_duration_ms:int; playback_mode:enum; client_surface:enum | none |
playback_seeked |
User seeked in episode | episode_id:string; podcast_id:string; entry_point:enum; from_seconds:float; to_seconds:float | seek_source:string; client_surface:enum | none |
session_restore_prompt |
Resume previous session prompt shown/acted | action:string | episode_id:string; podcast_id:string; position_seconds:float | none |
podcast_subscription_toggled |
User subscribed or unsubscribed | podcast_id:string; is_subscribed:bool | surface:string; source:string | none |
notification_permission_requested |
System permission prompt requested | — | surface:string | none |
notification_permission_decided |
User granted/denied notifications | granted:bool | — | none |
notification_tapped |
User opened a push/local notification | notification_type:string | podcast_id:string; episode_id:string; target_route:string | none |
notification_received |
Push/local notification delivered | notification_type:string | podcast_id:string; episode_id:string | none |
identity_reset |
Analytics identity cleared/reset | — | reason:string | none |
app_check_status |
App Check token attempt result, once per API session exchange that asks App Check (App Check is no longer fetched per launch or per request) | token_obtained:bool; provider:string | reason:string (play_integrity_<code>, http_<status>, too_many_attempts, network, other) |
none |
api_session_status |
API session result: a /auth/session exchange (kind=exchange), the first silent renewal of a token or a renewal problem (kind=renew), or a Turnstile step-up (kind=step_up). Categories only, never tokens or ids |
kind:enum; success:bool | trigger:enum (launch, expired, rejected, refresh, upgrade, step_up, retry); attestation:enum (appcheck, challenge, none); limited:bool; duration_ms:int; app_check_attempted:bool; renew_outcome:enum (renewed, proof_unavailable, rejected); step_up_outcome:enum (passed, challenge_failed, not_accepted); failure_reason:string; server_reason:string |
none |
first_episode_played |
First-ever play milestone | — | episode_id:string; podcast_id:string; entry_point:enum; hours_since_install:float | none |
home_surface_tapped |
First-class Home component tap | surface_component:string | rail_intent:string; content_id:string; position_index:int; items_count:int; mix_mode:string; podcast_id:string; podcast_name:string; episode_id:string; episode_title:string; click_target:string | none |
home_surface_impression |
Home rail/block became visible | surface_component:string | items_count:int; podcast_ids:list | none |
search_performed |
User ran show or episode search | surface:string; search_mode:enum; search_query:string; results_count:int | result_quality:enum; query_length:int | logged_by_policy |
search_result_tapped |
User tapped a search result | surface:string; result_type:enum | search_mode:enum; podcast_id:string; episode_id:string; position_index:int; search_query:string | logged_by_policy |
learn_card_action |
User acted on a Learn card | action:string; episode_id:string; podcast_id:string | position_index:int | none |
learn_screen_viewed |
Learn tab/screen opened | — | cards_available:int | none |
explore_screen_viewed |
Explore opened | — | tab:string | none |
explore_recommendation_tapped |
For You/trending card tapped | — | podcast_id:string; position_index:int; rail:string | none |
queue_modified |
Queue add/remove/reorder/clear | action:string | episode_id:string; podcast_id:string; queue_size:int; source:string | none |
episode_liked_toggled |
Like/unlike episode | episode_id:string; podcast_id:string; is_liked:bool | surface:string | none |
episode_mark_played |
User marked episode played/unplayed | episode_id:string; podcast_id:string; is_played:bool | surface:string | none |
download_requested |
User or Smart Downloads requested download | episode_id:string; podcast_id:string; source:string | wifi_only:bool | none |
download_completed |
Episode finished downloading | episode_id:string; podcast_id:string | source:string; bytes:long; duration_ms:long | none |
download_failed |
Download failed | error_type:string | episode_id:string; podcast_id:string; source:string; error_message:string | none |
smart_download_sync |
Smart Downloads sync ran | — | requested_count:int; completed_count:int; failed_count:int; cleaned_count:int; trigger:string | none |
show_notification_toggled |
Per-show notification preference changed | podcast_id:string; enabled:bool | — | none |
share_content |
User shared podcast/episode/app | content_type:enum | podcast_id:string; episode_id:string; channel:string; surface:string | none |
backup_restore_result |
Library backup or restore finished | action:string; success:bool | item_count:int; format:string; error_message:string | none |
feedback_submitted |
In-app feedback/NPS/review handoff | feedback_type:string | score:int; source:string | none |
library_destination_viewed |
Library hub or sub-destination opened | destination:string | item_count:int | none |
podcast_detail_viewed |
Podcast info screen opened | podcast_id:string | is_subscribed:bool | none |
episode_detail_viewed |
Episode info screen opened | episode_id:string; podcast_id:string | — | none |
nav_tab_clicked |
Bottom nav tab selected | tab:string | previous_tab:string | none |
settings_interaction |
Settings screen view or control | action:string | setting_key:string | none |
custom_theme_editor_opened |
Custom theme editor becomes visible | entry_point:string | — | none |
custom_theme_saved |
Custom theme preferences saved successfully | background_style:string | — | none |
home_hero_style_selected |
Home hero card style or its colors picked in Settings › Appearance › Home | hero_card_style:string; hero_colors:string; hero_style_combo:string; changed:string | previous_hero_style_combo:string | none |
recommendation_section_toggled |
A LeaveMeAlone switch flipped, or the Brief hidden from its card | section:string; enabled:bool; source:string; sections_off:int | — | none |
feature_announcement_action |
In-app/feature announcement viewed/dismissed/acted | action:string | feature_id:string; category:string | none |
offline_mode_entered |
App detected offline / offline UI | — | reason:string | none |
player_chrome_interaction |
Mini/full player, control bar, or playback-route action | surface:string; action:string | screen:string; volume_control_available:bool | none |
daily_briefing_action |
Briefing impression or interaction | action:string; region:string; date:string | content_id:string; source:string; chapter_index:int; chapter_title:string; method:string; playback_status:string; previous_region:string; episode_id:string; episode_title:string; podcast_id:string; podcast_title:string | none |
home_import_banner_action |
Import banner shown/clicked/dismissed | action:string | — | none |
android_auto_connected |
Auto session began | — | session_id:string | none |
android_auto_disconnected |
Auto session ended | — | session_id:string; duration_seconds:int | none |
android_auto_browse |
User browsed Auto media tree | node:string | action:string | none |
adaptive_ranking_status |
Ranking engine status/update | status:string | details:string | none |
learn_caught_up |
User reached Learn caught-up state | — | cards_remaining:int | none |
catalog_miss |
Catalog/lookup miss | lookup_type:string | key:string | none |
rss_refresh_failed |
RSS feed refresh failed | — | podcast_id:string; error_type:string | none |
progress_sync_anomaly |
Progress sync inconsistency detected | anomaly_type:string | episode_id:string | none |
late_night_safeguard_decision |
Late-night listening safeguard fired | decision:string | — | none |
proxy_fallback_triggered |
Image load fell back to proxy | — | reason:string | none |
support_page_viewed |
Support boxlore development screen opened (one per visit; the support-world experiment exposure) |
— | support_world:enum; assignment_source:enum; entry_point:string; local_hour:int; weekday:int | none |
support_tier_toggled |
Support tier artifact swiped, picked or lore inspected | tier_title:string; codename:string; amount:string | is_lore_inspect:bool; support_world:enum; assignment_source:enum; tier_index:int | none |
support_donate_clicked |
User tapped donate CTA button for a support tier | tier_title:string; codename:string; amount:string | support_world:enum; assignment_source:enum; tier_index:int; amount_usd:float; payments_live:bool | none |
support_entry_viewed |
Support boxlore entry shown in Settings: phone top-bar button or tablet category list (once per hub or list view; not on rotation) | surface:enum | — | none |
support_page_left |
Support boxlore page closed after a visit (never on rotation or resize) | support_world:enum; assignment_source:enum; dwell_ms:int; tiers_viewed:int; pay_tapped:bool | — | none |
surface is settings_top_bar or settings_category_list. support_world is control, neon, orbit, alchemy, abyss or sky. assignment_source is experiment (counted), fallback (flag not loaded or unknown, shows control) or debug_override (debug builds only). Read results from experiment rows only.
| property | set_when | notes |
|---|---|---|
| first_seen_at | first process | $set_once |
| onboarding_status | pending / completed | pending on first launch, completed when onboarding finishes |
| onboarding_method | ai_chat / manual_genre / search / import / skip | set with onboarding_status=completed |
| user_intent | onboarding complete | casual_browser (skip) / ai_guided_listener (AI chat) / targeted_listener (search) / selective_curator (manual genres) / migrating_power_user (import) |
| favorite_genres | onboarding complete (AI chat, manual genres) | genre list |
| listener_profile / genre_breadth / genre_enthusiasm | onboarding complete (manual genres) | derived from the picked genres (GenrePersonaLogic) |
| install_channel | first open | |
| referrer_raw / utm_source / share_token | first open, with the install referrer | $set_once, only when present |
| notifications_enabled | permission decide | |
| initial_podcasts_subscribed | onboarding complete | |
| first_play_at | first episode played | $set_once, with first_episode_played_logged=true |
| home_hero_card_style | launch + hero style change | block / cover / tide / duotone / halo / marquee; set for the default too, once per process |
| home_hero_colors | launch + hero colors change | dark / artwork / theme; none for Block |
| home_hero_style_combo | launch + any hero look change | style and colors in one value (cover_artwork, block); resent after an identity reset |
- Log
user_input_texton AI chat turns andsearch_queryon search events (pii_risk=logged_by_policy). - Before emission, scrub those fields: remove email addresses, password-like tokens, and any
http(s)audio/enclosure/media URLs. Keep the remaining free-text query/message. - Never emit emails, passwords, or audio/enclosure URLs as standalone property values.
entry_pointis required on allplayback_*events.