feat(sharing): track the share and invite funnel in PostHog
Closed
SA
1h ago+274+274+012345678901234567890123456789-18-18-01234567890123456789
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
| Event | Fires | Timeline | Properties |
|---|---|---|---|
share_dialog_opened | share dialog mounts (desktop, web, mobile) | sharer | resource_kind |
share_dialog_closed | share dialog unmounts | sharer | resource_kind, shared |
share_invite_created | a share creates an invitation, right after it's created | sharer | resource_kind, invitation_id |
invitation_created | Better Auth afterCreateInvitation (not on resend) | inviter | invitation_id |
invitation_accept_failed | accept endpoint's membership step refuses | inviter | invitation_id, reason (free_plan_limit, not_pending, add_member_failed) |
member_joined | every way into an org | member | join_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_joinedwith the sameinvitation_id. Settings invites for comparison:invitation_created→member_joined. - Share dialog drop-off:
share_dialog_opened→share_dialog_closedwithshared: true. - How people join, and who invites:
member_joinedbyjoin_method, and byinvited_byper org.
Notes
- Event names live once in
@superset/shared/sharing(shareUiEvent, which returns[name, properties]). Each app callstrack(...shareUiEvent(event, kind))inline. ShareAccess(desktop and web) takes an optionalonEventand reports opened on mount and closed on unmount. Mobile's share sheet stays mounted under the invite screen, souseShareSessionreports open and close, andshare()marks the session shared.member_joinedis captured at each call site, not inafterAddMember: that hook can't tell how someone joined, and during the custom accept flow the invitation is stillpendingwhen it runs.- Server captures never throw. The auth side awaits
posthog.flush()(3s timeout), the same asbilling-analytics.ts.
Possibly unrelated bugs, not fixed here:
afterAddMembersends "You've been added to {org}" only if it finds anacceptedinvitation, but the accept endpoint sets that status afteraddMemberreturns, 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 nouser.signed_upevent for people who join by invite.
How I tested it
bun run typecheckpasses inpackages/shared,packages/ui,packages/auth,packages/trpc,apps/webandapps/desktop. Inapps/mobilenone of the type errors are in changed files; the existing 187 come from missing@superset/host-service/routerbuild output.- Tests pass:
packages/sharedsharing (19),packages/auth(91),packages/trpcpage / cloud-workspace / organization / lib (442). biome checkis 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 lintandbun run typecheckpass (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.