feat(studio): add click tracking for top bar buttons (#45414)

## Summary

Adds PostHog click/open tracking for every interactive element in the
Studio top bar. Previously only 5 of ~16 surfaces were tracked.

### New events (16)

| Event | Surface |
|---|---|
| `home_logo_clicked` | Supabase logo |
| `header_back_to_dashboard_clicked` | Mobile back chevron |
| `header_exceeding_usage_badge_clicked` | "Exceeding usage limits"
badge |
| `organization_dropdown_opened` | Org dropdown trigger |
| `project_dropdown_opened` | Project dropdown trigger |
| `branch_dropdown_opened` | Branch dropdown trigger |
| `merge_request_button_clicked` | MR trigger (separate from existing
success event) |
| `connect_button_clicked` | Connect CTA |
| `feedback_dropdown_opened` | Feedback dropdown trigger |
| `advisor_button_clicked` | Advisor toggle |
| `inline_editor_button_clicked` | SQL editor toggle |
| `assistant_button_clicked` | AI Assistant toggle |
| `user_dropdown_opened` | Account dropdown |
| `local_dropdown_opened` | Local-dev settings dropdown |
| `local_version_popover_opened` | CLI version popover |

### Notes
- Uses `useTrack` (per `telemetry-standards`), all event names use
approved `_clicked` / `_opened` verbs.
- Dropdown `onOpenChange` handlers guard against Radix's double-fire by
only tracking when `open === true`.
- `merge_request_button_clicked` fires on the trigger click; the
existing `branch_create_merge_request_button_clicked` continues to fire
on successful MR creation.
- Pre-existing tracked surfaces (`command_menu_opened`,
`help_button_clicked`, `header_upgrade_cta_clicked`,
`send_feedback_button_clicked`) are unchanged.

## Test plan

- [x] Spot-check each event fires once per interaction in PostHog Live
Events
- [x] Verify no double-fire on dropdown close

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Added telemetry tracking for many header/navigation interactions
(logo, back-to-dashboard, usage badge,
connect/merge/advisor/assistant/inline-editor buttons, and multiple
dropdowns/popovers).
* **Tests**
* Updated tests to stub telemetry calls so UI tests remain stable and
deterministic.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Mert YEREKAPAN
2026-05-05 18:12:51 +02:00
committed by GitHub
parent 2c892acec4
commit da81b2f14d
16 changed files with 265 additions and 8 deletions
@@ -6,6 +6,7 @@ import { Button, cn } from 'ui'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
import { useAppStateSnapshot } from '@/state/app-state'
interface ConnectButtonProps {
@@ -17,6 +18,7 @@ export const ConnectButton = ({ buttonType = 'default', className }: ConnectButt
const { data: selectedProject } = useSelectedProjectQuery()
const { setConnectSheetSource } = useAppStateSnapshot()
const isActiveHealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_HEALTHY
const track = useTrack()
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
@@ -27,6 +29,7 @@ export const ConnectButton = ({ buttonType = 'default', className }: ConnectButt
className={cn('rounded-full', className)}
icon={<Plug className="rotate-90" />}
onClick={() => {
track('header_connect_button_clicked')
setConnectSheetSource('header_button')
setShowConnect(true)
}}
@@ -60,6 +60,8 @@ vi.mock('./App/FeaturePreview/FeaturePreviewContext', () => ({
}),
}))
vi.mock('@/lib/telemetry/track', () => ({ useTrack: () => vi.fn() }))
vi.mock('ui', async () => {
const React = await import('react')
@@ -20,6 +20,7 @@ import {
import { ButtonTooltip } from '../ui/ButtonTooltip'
import { useFeaturePreviewModal } from './App/FeaturePreview/FeaturePreviewContext'
import { ProfileImage } from '@/components/ui/ProfileImage'
import { useTrack } from '@/lib/telemetry/track'
import { useAppStateSnapshot } from '@/state/app-state'
export const LocalDropdown = ({
@@ -33,9 +34,14 @@ export const LocalDropdown = ({
const { theme, setTheme } = useTheme()
const appStateSnapshot = useAppStateSnapshot()
const { toggleFeaturePreviewModal } = useFeaturePreviewModal()
const track = useTrack()
return (
<DropdownMenu>
<DropdownMenu
onOpenChange={(open) => {
if (open) track('header_local_dropdown_opened')
}}
>
<DropdownMenuTrigger className={cn('border shrink-0 px-3', triggerClassName)} asChild>
<ButtonTooltip
type="default"
@@ -23,6 +23,7 @@ import { ProfileImage } from '@/components/ui/ProfileImage'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { IS_PLATFORM } from '@/lib/constants'
import { useProfileNameAndPicture } from '@/lib/profile'
import { useTrack } from '@/lib/telemetry/track'
import { useAppStateSnapshot } from '@/state/app-state'
export function UserDropdown({
@@ -39,9 +40,14 @@ export function UserDropdown({
const { username, avatarUrl, primaryEmail, isLoading } = useProfileNameAndPicture()
const { toggleFeaturePreviewModal } = useFeaturePreviewModal()
const track = useTrack()
return (
<DropdownMenu>
<DropdownMenu
onOpenChange={(open) => {
if (open) track('header_user_dropdown_opened')
}}
>
<DropdownMenuTrigger asChild className={cn('border shrink-0 px-3', triggerClassName)}>
<ButtonTooltip
type="default"
@@ -7,10 +7,12 @@ import { useAdvisorSignals } from '@/components/ui/AdvisorPanel/useAdvisorSignal
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useProjectLintsQuery } from '@/data/lint/lint-query'
import { useNotificationsV2Query } from '@/data/notifications/notifications-v2-query'
import { useTrack } from '@/lib/telemetry/track'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
export const AdvisorButton = ({ projectRef }: { projectRef?: string }) => {
const { toggleSidebar, activeSidebar } = useSidebarManagerSnapshot()
const track = useTrack()
const { data: lints } = useProjectLintsQuery({ projectRef })
const { data: signalItems } = useAdvisorSignals({ projectRef })
@@ -36,6 +38,7 @@ export const AdvisorButton = ({ projectRef }: { projectRef?: string }) => {
const isOpen = activeSidebar?.id === SIDEBAR_KEYS.ADVISOR_PANEL
const handleClick = () => {
track('header_advisor_button_clicked')
toggleSidebar(SIDEBAR_KEYS.ADVISOR_PANEL)
}
@@ -2,6 +2,7 @@ import { AiIconAnimation, cn, KeyboardShortcut } from 'ui'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useTrack } from '@/lib/telemetry/track'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
@@ -9,6 +10,7 @@ import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
export const AssistantButton = () => {
const { activeSidebar, toggleSidebar } = useSidebarManagerSnapshot()
const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
const track = useTrack()
const isOpen = activeSidebar?.id === SIDEBAR_KEYS.AI_ASSISTANT
@@ -22,6 +24,7 @@ export const AssistantButton = () => {
isOpen && 'bg-foreground text-background'
)}
onClick={() => {
track('header_assistant_button_clicked')
toggleSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
}}
tooltip={{
@@ -9,6 +9,7 @@ import { useEmbeddedCloseHandler } from './useEmbeddedCloseHandler'
import { useBranchesQuery } from '@/data/branches/branches-query'
import type { Branch } from '@/data/branches/branches-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useTrack } from '@/lib/telemetry/track'
import { useAppStateSnapshot } from '@/state/app-state'
interface BranchDropdownProps {
@@ -28,6 +29,12 @@ export const BranchDropdown = ({
const [open, setOpen] = useState(false)
const close = useEmbeddedCloseHandler(embedded, onClose, setOpen)
const track = useTrack()
const handleOpenChange = (next: boolean) => {
if (next) track('header_branch_dropdown_opened')
setOpen(next)
}
const projectRef = projectDetails?.parent_project_ref || ref
@@ -100,7 +107,7 @@ export const BranchDropdown = ({
linkClassName="flex items-center gap-2 shrink-0"
commandContent={commandContent}
open={open}
onOpenChange={setOpen}
onOpenChange={handleOpenChange}
/>
)
}
@@ -3,6 +3,7 @@ import { cn, KeyboardShortcut } from 'ui'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useTrack } from '@/lib/telemetry/track'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
@@ -16,8 +17,10 @@ const InlineEditorKeyboardTooltip = () => {
export const InlineEditorButton = () => {
const { activeSidebar, toggleSidebar } = useSidebarManagerSnapshot()
const isOpen = activeSidebar?.id === SIDEBAR_KEYS.EDITOR_PANEL
const track = useTrack()
const handleClick = () => {
track('header_inline_editor_button_clicked')
toggleSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
}
@@ -12,6 +12,7 @@ import PartnerIcon from '@/components/ui/PartnerIcon'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useTrack } from '@/lib/telemetry/track'
interface OrganizationDropdownProps {
embedded?: boolean
@@ -40,6 +41,12 @@ export const OrganizationDropdown = ({
const [open, setOpen] = useState(false)
const close = useEmbeddedCloseHandler(embedded, onClose, setOpen)
const track = useTrack()
const handleOpenChange = (next: boolean) => {
if (next) track('header_organization_dropdown_opened')
setOpen(next)
}
if (isLoadingOrganizations && !embedded)
return <ShimmeringLoader className="p-2 md:mr-2 w-[90px]" />
@@ -84,7 +91,7 @@ export const OrganizationDropdown = ({
}
commandContent={commandContent}
open={open}
onOpenChange={setOpen}
onOpenChange={handleOpenChange}
/>
)
}
@@ -21,6 +21,7 @@ import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganizati
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { IS_PLATFORM } from '@/lib/constants'
import type { ManagedBy } from '@/lib/constants/infrastructure'
import { useTrack } from '@/lib/telemetry/track'
// --- Sub-components ---
@@ -140,6 +141,7 @@ export const ProjectDropdown = ({
const selectedProject = parentProject ?? project
const projectCreationEnabled = useIsFeatureEnabled('projects:create')
const track = useTrack()
const [open, setOpen] = useState(false)
const close = useEmbeddedCloseHandler(embedded, onClose, setOpen)
@@ -153,7 +155,12 @@ export const ProjectDropdown = ({
if (!embedded) return <ShimmeringLoader className="p-2 md:mr-2 md:w-[90px]" />
}
const handleSetOpen = embedded ? (_value: boolean) => onClose?.() : setOpen
const handleSetOpen = embedded
? (_value: boolean) => onClose?.()
: (next: boolean) => {
if (next) track('header_project_dropdown_opened')
setOpen(next)
}
const selectorProps = {
open,
@@ -17,6 +17,7 @@ import { getSupportLinkQueryParams } from '@/components/ui/HelpPanel/HelpPanel.u
import { HelpSection } from '@/components/ui/HelpPanel/HelpSection'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useTrack } from '@/lib/telemetry/track'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
@@ -28,6 +29,7 @@ export const FeedbackDropdown = ({ className }: { className?: string }) => {
const { openSidebar } = useSidebarManagerSnapshot()
const [isOpen, setIsOpen] = useState(false)
const [stage, setStage] = useState<'select' | 'issue-options' | 'widget'>('select')
const track = useTrack()
const projectRef = project?.parent_project_ref ?? (router.query.ref as string | undefined)
const supportLinkQueryParams = getSupportLinkQueryParams(
@@ -41,6 +43,7 @@ export const FeedbackDropdown = ({ className }: { className?: string }) => {
modal={false}
open={isOpen}
onOpenChange={(e) => {
if (e) track('header_feedback_dropdown_opened')
setIsOpen(e)
if (!e) setStage('select')
}}
@@ -8,10 +8,12 @@ import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
export const HomeIcon = ({ className }: { className?: string }) => {
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { data: organizations } = useOrganizationsQuery()
const track = useTrack()
const largeLogo = useIsFeatureEnabled('branding:large_logo')
@@ -31,7 +33,11 @@ export const HomeIcon = ({ className }: { className?: string }) => {
const href = IS_PLATFORM ? getDefaultOrgRedirect() : '/project/default'
return (
<Link href={href} className={cn('items-center justify-center shrink-0 flex', className)}>
<Link
href={href}
onClick={() => track('header_home_logo_clicked')}
className={cn('items-center justify-center shrink-0 flex', className)}
>
<img
alt="Supabase"
src={`${router.basePath}/img/supabase-logo.svg`}
@@ -30,6 +30,7 @@ import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { IS_PLATFORM } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
@@ -69,6 +70,7 @@ export const LayoutHeader = ({
const { ref: projectRef, slug } = useParams()
const { data: selectedProject } = useSelectedProjectQuery()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const track = useTrack()
const commandMenuEnabled = useIsShortcutEnabled(SHORTCUT_IDS.COMMAND_MENU_OPEN)
@@ -104,6 +106,7 @@ export const LayoutHeader = ({
<div className="flex items-center justify-center border-r flex-0 md:hidden h-full aspect-square">
<Link
href={backToDashboardURL}
onClick={() => track('header_back_to_dashboard_clicked')}
className="flex items-center justify-center border-none bg-transparent! rounded-md min-w-[30px] w-[30px] h-[30px] text-foreground-lighter hover:text-foreground transition-colors"
>
<ChevronLeft strokeWidth={1.5} size={16} />
@@ -160,7 +163,10 @@ export const LayoutHeader = ({
{exceedingLimits && (
<div className="ml-2">
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
<Link
href={`/org/${selectedOrganization?.slug}/usage`}
onClick={() => track('header_exceeding_usage_badge_clicked')}
>
<Badge variant="destructive">Exceeding usage limits</Badge>
</Link>
</div>
@@ -25,9 +25,11 @@ import { DocsButton } from '@/components/ui/DocsButton'
import { InlineLink } from '@/components/ui/InlineLink'
import { useCLIReleaseVersionQuery } from '@/data/misc/cli-release-version-query'
import { DOCS_URL } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
export const LocalVersionPopover = () => {
const { data, isSuccess } = useCLIReleaseVersionQuery()
const track = useTrack()
const currentCliVersion = data?.current
const latestCliVersion = data?.latest
const hasLatestCLIVersion = isSuccess && !!latestCliVersion
@@ -49,7 +51,11 @@ export const LocalVersionPopover = () => {
if (!isSuccess || !currentCliVersion) return null
return (
<Popover_Shadcn_>
<Popover_Shadcn_
onOpenChange={(open) => {
if (open) track('header_local_version_popover_opened')
}}
>
<PopoverTrigger_Shadcn_ className="flex items-center">
<Badge variant={isBeta ? 'warning' : hasUpdate ? 'success' : 'default'}>
{isBeta ? 'Beta' : hasUpdate ? 'Update available' : 'Latest'}
@@ -9,6 +9,7 @@ import { useBranchesQuery } from '@/data/branches/branches-query'
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useTrack } from '@/lib/telemetry/track'
export const MergeRequestButton = () => {
const { ref } = useParams()
@@ -21,6 +22,7 @@ export const MergeRequestButton = () => {
const { data: branches } = useBranchesQuery({ projectRef }, { enabled: Boolean(projectDetails) })
const { mutate: sendEvent } = useSendEventMutation()
const track = useTrack()
const { mutate: updateBranch, isPending: isUpdating } = useBranchUpdateMutation({
onError: () => {
@@ -37,6 +39,7 @@ export const MergeRequestButton = () => {
const buttonLabel = hasReviewRequested ? 'Review merge request' : 'Open merge request'
const handleClick = () => {
track('header_merge_request_button_clicked', { hasReviewRequested })
if (hasReviewRequested) {
router.push(`/project/${selectedBranch.project_ref}/merge`)
} else {