mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
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:
@@ -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,
|
||||
|
||||
+3
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user