reference

PostHog event catalog

Catalog of every product-analytics event in the PostHogEvent union in apps/frontend/src/lib/analytics.ts. Events fire via:

import { trackProductEvent } from '@/lib/analytics'

trackProductEvent('thing_done', { season_id, source })

The helper is a no-op when PostHog isn't initialized (no key, SSR, errors), so it's safe to call from any client component without guards. Initialization happens once via components/analytics/PostHogProvider.tsx mounted in the root layout; the provider reads NEXT_PUBLIC_POSTHOG_KEY and uses person_profiles: 'identified_only' so anonymous traffic doesn't create person profiles. identifyUser() is called after sign-in to link the anonymous distinct_id to a Supabase user id.

A small set of events — graphene_plus_subscription_active, graphene_plus_subscription_canceled, episode_tip_paid — are captured server-side from the Stripe webhook in apps/backend/src/routes/payment.ts via services/analytics-server.ts. They're listed in the union for type safety + documentation; do not fire them from the frontend (would double-count).

Three events also dual-write to our self-hosted funnel table for /dashboard/admin/monetization so the conversion-rate denominator doesn't depend on PostHog's API: graphene_locked_chapter_impression, graphene_plus_modal_opened, graphene_plus_checkout_started. See services/monetization-funnel.ts and the funnel-stage mapper at the bottom of analytics.ts.

There is no super-admin exclusion at the PostHog layer today — admins viewing the production app generate the same events real users do. Filter in PostHog views if this matters.

For the convention on naming + payload shape for new events, see docs/ai/CONVENTIONS.md → Tracking events via PostHog.


Citizen Science

DreamPro template, replication, build, and video flows. Source: apps/frontend/src/app/dreampro/.

EventSource surface(s)Typical payloadNotes
template_vieweddreampro/templates/[id]/page.tsx{ template_id, slug }
template_cloneddreampro/templates/[id]/page.tsx{ template_id, replication_id }
clone_progress_updated{ replication_id, progress }// not currently fired
replication_submittedcomponents/dreampro/ReplicationModal.tsx{ template_id, replication_id, outcome }
build_submittedcomponents/dreampro/BuildModal.tsx{ template_id, build_id }
video_submittedcomponents/dreampro/VideoSubmissionModal.tsx{ video_id, source: 'replication' | 'build' }
video_upvoted{ video_id }// not currently fired

Participant flows

Opt-in to the citizen-scientist map, family teams, classrooms, meetups.

EventSource surface(s)Typical payloadNotes
citizen_scientist_opted_indreampro/citizen-scientist/page.tsx{ participant_id, city }City-centroid only; never exact lat/lng
family_team_registereddreampro/family/page.tsx{ team_id, member_count }
classroom_registereddreampro/classroom/page.tsx{ classroom_id }
meetup_registereddreampro/meetup-organizer/page.tsx{ organizer_id }
meetup_event_createddreampro/meetups/[id]/page.tsx{ event_id, meetup_id }
meetup_event_rsvpeddreampro/meetups/[id]/page.tsx{ event_id, response }

Sponsors & opportunities

EventSource surface(s)Typical payloadNotes
sponsor_applieddreampro/sponsors/apply/page.tsx{ application_id, org_name }
opportunity_vieweddreampro/opportunities/[slug]/page.tsx{ opportunity_id, slug }
outreach_sentdreampro/sponsorable/page.tsx{ outreach_id, sponsor_id }

Competition

EventSource surface(s)Typical payloadNotes
competition_leaderboard_vieweddreampro/competition/[slug]/page.tsx{ competition_slug }
competition_scores_recomputed{ competition_slug, participant_count }// not currently fired

Map

EventSource surface(s)Typical payloadNotes
map_vieweddreampro/map/page.tsx{}
map_participant_clicked{ participant_id }// not currently fired

Kit waitlist

EventSource surface(s)Typical payloadNotes
kit_waitlist_signupdreampro/kits/page.tsx{ kit_slug, email }

Extension

EventSource surface(s)Typical payloadNotes
extension_cta_clickedjq-extension/page.tsx{ source: 'hero' | 'footer' | ... }Listed twice in the union — single category here

Auth & onboarding

EventSource surface(s)Typical payloadNotes
signup_completedauth/signup/page.tsx{ user_id, method }
signin_completedauth/signin/page.tsx{ user_id, method }
signup_started{ source }// not currently fired (GA sign_up_started covers this surface today)

Journal

EventSource surface(s)Typical payloadNotes
entry_publisheddashboard/entries/new/page.tsx{ entry_id, notebook_id, word_count }
entry_updateddashboard/entries/new/page.tsx{ entry_id, notebook_id }

Goals & dreams

EventSource surface(s)Typical payloadNotes
dream_createddashboard/dreampro/page.tsx{ dream_id, category }
goal_createddashboard/goals/new/page.tsx{ goal_id, category }
notebook_createddashboard/notebooks/new/page.tsx{ notebook_id, notebook_type }

Social

EventSource surface(s)Typical payloadNotes
jq_bridge_invitation_sentdashboard/jq-bridge/page.tsx{ invitation_id, recipient_email }
jq_bridge_invitation_accepted{ invitation_id }// not currently fired

Odessa story engine

The personalized story engine — journals → metaphorical graphic novel → optional Graphene port.

EventSource surface(s)Typical payloadNotes
odessa_seed_generateddashboard/odessa/page.tsx{ seed_id, length }
odessa_story_starteddashboard/odessa/page.tsx{ notebook_id, length }
odessa_full_story_generateddashboard/odessa/page.tsx{ notebook_id, page_count }
odessa_next_episodedashboard/notebooks/[id]/page.tsx{ notebook_id, episode_number }
odessa_branch_chosendashboard/notebooks/[id]/page.tsx{ notebook_id, branch_id }
odessa_story_shareddashboard/notebooks/[id]/page.tsx{ notebook_id, source }
odessa_story_ported_to_graphenedashboard/notebooks/[id]/page.tsx{ notebook_id, season_id }
season_link_copiedseasons/[id]/SeasonClient.tsx{ season_id, source: 'public_reader_finished' | ... }
story_link_copiedstories/[id]/page.tsx{ story_id, source }
season_publish_toggledseasons/[id]/SeasonClient.tsx{ season_id, action: 'publish' | 'unpublish', source: 'publish_strip' }

Import funnel

EventSource surface(s)Typical payloadNotes
journal_import_starteddashboard/import/page.tsx, dashboard/import/apple-notes/page.tsx{ source: 'apple_notes' | 'csv' | ... }
journal_import_completedsame as above{ source, entry_count }
see_your_story_visitedsee-your-story/page.tsx{}Funnel-shaping landing

General engagement

EventSource surface(s)Typical payloadNotes
vision_page_viewedvision/page.tsx, hum/page.tsx, hum/prototype/page.tsx{ slug }
docs_page_viewed{ slug }// not currently fired
extension_cta_clicked(see Extension above)(see above)Duplicated divider — same event

Graphene / Story Seasons

The public Graphene slate + per-season detail pages. The trailer hero + poster cards drive most engagement events.

EventSource surface(s)Typical payloadNotes
graphene_poster_hovercomponents/graphene/PosterCard.tsx{ season_id, dwell_ms, had_trailer }Only fires after 400ms dwell
graphene_poster_clickedcomponents/graphene/PosterCard.tsx{ season_id }
graphene_primary_cta_clickedgraphene/GrapheneClient.tsx{ season_id, source: 'override' | 'freshest_trailer' }
graphene_subscribe_clickedgraphene/GrapheneClient.tsx{ location: 'hero' | 'closing_band' | 'modal_submit' }
graphene_filter_tag_clickedcomponents/graphene/RotatingHeroTags.tsx{ kind: 'genre' | 'theme' | 'topic', value }
graphene_stream_card_clickedcomponents/graphene/GrapheneStreamCard.tsx{ season_id, slot_index }Stream-injected promo cards
graphene_filter_clearedgraphene/GrapheneClient.tsx{ previous_filter }
graphene_taste_savedcomponents/graphene/GrapheneTasteIntro.tsx{ genre_count, tone_count, pace }
graphene_taste_skippedcomponents/graphene/GrapheneTasteIntro.tsx{}
season_trailer_viewcomponents/seasons/TrailerHero.tsx{ season_id, kind, surface }Impression — gated on ≥50% visible for ~1s via useImpressionRef, NOT on mount, so instant-bounce ad traffic doesn't inflate the play-rate denominator
season_trailer_playcomponents/seasons/TrailerHero.tsx{ season_id }
season_trailer_completedcomponents/seasons/TrailerHero.tsx{ season_id, duration_ms }

Graphene+ ($5/mo listener subscription)

The paywall + checkout funnel. Three events here also dual-write to monetization_funnel_events for server-side conversion-rate reporting (see intro).

EventSource surface(s)Typical payloadNotes
graphene_plus_modal_openedcomponents/graphene/GraphenePlusModal.tsx{ source: 'locked_chapter_row' | 'locked_chapter_card' | 'hero_pill' | 'letters_priority' | 'dashboard' | 'subscribed_query_param' | 'profile' | 'other' }Dual-write → modal_opened
graphene_plus_checkout_startedcomponents/graphene/GraphenePlusModal.tsx{ source, plan_key, experiment_key?, variant_key? }Dual-write → checkout_started
graphene_locked_chapter_impressionseasons/[id]/SeasonClient.tsx{ season_id, episode_number, source: 'chapter_row' | 'chapter_card' }Dual-write → locked_impression
graphene_plus_subscription_activeservices/analytics-server.ts (Stripe webhook){ plan_key, stripe_price_id, source: 'webhook' }Server-side only
graphene_plus_subscription_canceledservices/analytics-server.ts (Stripe webhook){ plan_key, source: 'webhook', cancel_at_period_end, ended_immediately, current_period_end }Server-side only

Per-episode tipping

Mirrors the subscription funnel shape — frontend captures the intent, the webhook captures the success.

EventSource surface(s)Typical payloadNotes
episode_tip_startedcomponents/seasons/TipChapterModal.tsx{ season_id, episode_number, amount_cents }
episode_tip_paidservices/analytics-server.ts (Stripe webhook){ season_id, episode_number, amount_cents, source: 'webhook' }Server-side only
POSTHOG EVENTS — Docs | HiveJournal