feat(sharing): track the share and invite funnel in PostHog

Closed
SA
1h ago
+274-18

What & why

We couldn't tell whether sharing a page or workspace leads to new paid seats, or how people end up in an org. PostHog had no invite, share or membership events, and the database loses which invites came from sharing once they're accepted: convertPendingShares clears invitation_id.

This adds a small set of events, each fired where the code already knows the answer. It counts from deploy onward.

Events

EventFiresTimelineProperties
share_dialog_openedshare dialog mounts (desktop, web, mobile)sharerresource_kind
share_dialog_closedshare dialog unmountssharerresource_kind, shared
share_invite_createda share creates an invitation, right after it's createdsharerresource_kind, invitation_id
invitation_createdBetter Auth afterCreateInvitation (not on resend)inviterinvitation_id
invitation_accept_failedaccept endpoint's membership step refusesinviterinvitation_id, reason (free_plan_limit, not_pending, add_member_failed)
member_joinedevery way into an orgmemberjoin_method (created_org, invite, domain, admin_add), invited_by, invitation_id

Server events also carry the organization group. The client events get the app from PostHog's own $lib / $os.

How the questions get answered

  • Does sharing drive paid seats: share_invite_created → member_joined with the same invitation_id. Settings invites for comparison: invitation_created → member_joined.
  • Share dialog drop-off: share_dialog_opened → share_dialog_closed with shared: true.
  • How people join, and who invites: member_joined by join_method, and by invited_by per org.

Notes

  • Event names live once in @superset/shared/sharing (shareUiEvent, which returns [name, properties]). Each app calls track(...shareUiEvent(event, kind)) inline.
  • ShareAccess (desktop and web) takes an optional onEvent and reports opened on mount and closed on unmount. Mobile's share sheet stays mounted under the invite screen, so useShareSession reports open and close, and share() marks the session shared.
  • member_joined is captured at each call site, not in afterAddMember: that hook can't tell how someone joined, and during the custom accept flow the invitation is still pending when it runs.
  • Server captures never throw. The auth side awaits posthog.flush() (3s timeout), the same as billing-analytics.ts.

Possibly unrelated bugs, not fixed here:

  • afterAddMember sends "You've been added to {org}" only if it finds an accepted invitation, but the accept endpoint sets that status after addMember returns, so on a first acceptance it likely never sends.
  • The accept endpoint creates new users with a direct insert, which skips databaseHooks.user.create: no welcome email and no user.signed_up event for people who join by invite.

How I tested it

  • bun run typecheck passes in packages/shared, packages/ui, packages/auth, packages/trpc, apps/web and apps/desktop. In apps/mobile none of the type errors are in changed files; the existing 187 come from missing @superset/host-service/router build output.
  • Tests pass: packages/shared sharing (19), packages/auth (91), packages/trpc page / cloud-workspace / organization / lib (442).
  • biome check is clean on all changed files.
  • Not yet verified in PostHog. Before merging, in each app: open and close a share dialog, share with a new email, accept the invite, and confirm the events arrive.

Checklist

  • PR title follows conventional commits (type(scope): subject)
  • bun run lint and bun run typecheck pass (biome on changed files, typecheck per touched package; full lint left to CI)
  • "Allow edits from maintainers" is checked on fork PRs (n/a, same repo)
  • I have signed the CLA

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Organization activity now includes member joins, organization creation, invitations, and invitation acceptance outcomes.
    • Sharing activity is tracked for pages and cloud workspaces, including when sharing is opened, closed, or used to invite someone.
  • Bug Fixes
    • Invitation acceptance now distinguishes invitations that are no longer pending and failures caused by free-plan member limits.