Feat/add tracking and phflag for new home (#38853)
* add phflag * track homev2 clicks * track v2 drags * remove old home tracking and extra visibility tracking * add advisor, usage, and report clicks * fix import type * track remove charts from home * rename actions to follow conventions * fix lints * simplify activity stats and add issue count property * reduce useSelectedProjectQuery * revert onClick and improve event properties * Fix broken images --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
@@ -62,6 +62,10 @@ export const ActivityStats = () => {
|
||||
href={`/project/${ref}/database/migrations`}
|
||||
icon={<Database size={18} strokeWidth={1.5} className="text-foreground" />}
|
||||
label={<span>Last migration</span>}
|
||||
trackingProperties={{
|
||||
stat_type: 'migrations',
|
||||
stat_value: migrationsData?.length ?? 0,
|
||||
}}
|
||||
value={
|
||||
isLoadingMigrations ? (
|
||||
<Skeleton className="h-6 w-24" />
|
||||
@@ -81,6 +85,10 @@ export const ActivityStats = () => {
|
||||
href={`/project/${ref}/database/backups/scheduled`}
|
||||
icon={<Archive size={18} strokeWidth={1.5} className="text-foreground" />}
|
||||
label={<span>Last backup</span>}
|
||||
trackingProperties={{
|
||||
stat_type: 'backups',
|
||||
stat_value: backupsData?.backups?.length ?? 0,
|
||||
}}
|
||||
value={
|
||||
isLoadingBackups ? (
|
||||
<Skeleton className="h-6 w-24" />
|
||||
@@ -103,6 +111,10 @@ export const ActivityStats = () => {
|
||||
href={`/project/${ref}/branches`}
|
||||
icon={<GitBranch size={18} strokeWidth={1.5} className="text-foreground" />}
|
||||
label={<span>{isDefaultProject ? 'Recent branch' : 'Branch Created'}</span>}
|
||||
trackingProperties={{
|
||||
stat_type: 'branches',
|
||||
stat_value: branchesData?.length ?? 0,
|
||||
}}
|
||||
value={
|
||||
isLoadingBranches ? (
|
||||
<Skeleton className="h-6 w-24" />
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from 'components/interfaces/Linter/Linter.utils'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
||||
import {
|
||||
AiIconAnimation,
|
||||
@@ -32,6 +34,8 @@ export const AdvisorSection = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: lints, isLoading: isLoadingLints } = useProjectLintsQuery({ projectRef })
|
||||
const snap = useAiAssistantStateSnapshot()
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
|
||||
const [selectedLint, setSelectedLint] = useState<Lint | null>(null)
|
||||
|
||||
@@ -54,11 +58,40 @@ export const AdvisorSection = () => {
|
||||
|
||||
const handleAskAssistant = useCallback(() => {
|
||||
snap.toggleAssistant()
|
||||
}, [snap])
|
||||
if (projectRef && organization?.slug) {
|
||||
sendEvent({
|
||||
action: 'home_advisor_ask_assistant_clicked',
|
||||
properties: {
|
||||
issues_count: totalErrors,
|
||||
},
|
||||
groups: {
|
||||
project: projectRef,
|
||||
organization: organization.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [snap, sendEvent, projectRef, organization, totalErrors])
|
||||
|
||||
const handleCardClick = useCallback((lint: Lint) => {
|
||||
setSelectedLint(lint)
|
||||
}, [])
|
||||
const handleCardClick = useCallback(
|
||||
(lint: Lint) => {
|
||||
setSelectedLint(lint)
|
||||
if (projectRef && organization?.slug) {
|
||||
sendEvent({
|
||||
action: 'home_advisor_issue_card_clicked',
|
||||
properties: {
|
||||
issue_category: lint.categories[0] || 'UNKNOWN',
|
||||
issue_name: lint.name,
|
||||
issues_count: totalErrors,
|
||||
},
|
||||
groups: {
|
||||
project: projectRef,
|
||||
organization: organization.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[sendEvent, projectRef, organization]
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -111,6 +144,19 @@ export const AdvisorSection = () => {
|
||||
open: true,
|
||||
initialInput: createLintSummaryPrompt(lint),
|
||||
})
|
||||
if (projectRef && organization?.slug) {
|
||||
sendEvent({
|
||||
action: 'home_advisor_fix_issue_clicked',
|
||||
properties: {
|
||||
issue_category: lint.categories[0] || 'UNKNOWN',
|
||||
issue_name: lint.name,
|
||||
},
|
||||
groups: {
|
||||
project: projectRef,
|
||||
organization: organization.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
tooltip={{
|
||||
content: { side: 'bottom', text: 'Help me fix this issue' },
|
||||
|
||||
@@ -23,7 +23,9 @@ import { AnalyticsInterval } from 'data/analytics/constants'
|
||||
import { useContentInfiniteQuery } from 'data/content/content-infinite-query'
|
||||
import { Content } from 'data/content/content-query'
|
||||
import { useContentUpsertMutation } from 'data/content/content-upsert-mutation'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import type { Dashboards } from 'types'
|
||||
@@ -35,6 +37,8 @@ export function CustomReportSection() {
|
||||
const endDate = dayjs().toISOString()
|
||||
const { ref } = useParams()
|
||||
const { profile } = useProfile()
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
|
||||
const { data: reportsData } = useContentInfiniteQuery(
|
||||
{ projectRef: ref, type: 'report', name: 'Home', limit: 1 },
|
||||
@@ -185,6 +189,20 @@ export function CustomReportSection() {
|
||||
content: newReport,
|
||||
},
|
||||
})
|
||||
|
||||
if (ref && organization?.slug) {
|
||||
sendEvent({
|
||||
action: 'home_custom_report_block_added',
|
||||
properties: {
|
||||
block_id: snippet.id,
|
||||
position: 0,
|
||||
},
|
||||
groups: {
|
||||
project: ref,
|
||||
organization: organization.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
const current = [...editableReport.layout]
|
||||
@@ -193,16 +211,46 @@ export function CustomReportSection() {
|
||||
const updated = { ...editableReport, layout: current }
|
||||
setEditableReport(updated)
|
||||
persistReport(updated)
|
||||
|
||||
if (ref && organization?.slug) {
|
||||
sendEvent({
|
||||
action: 'home_custom_report_block_added',
|
||||
properties: {
|
||||
block_id: snippet.id,
|
||||
position: current.length - 1,
|
||||
},
|
||||
groups: {
|
||||
project: ref,
|
||||
organization: organization.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveChart = ({ metric }: { metric: { key: string } }) => {
|
||||
if (!editableReport) return
|
||||
const removedChart = editableReport.layout.find(
|
||||
(x) => x.attribute === (metric.key as unknown as Dashboards.Chart['attribute'])
|
||||
)
|
||||
const nextLayout = editableReport.layout.filter(
|
||||
(x) => x.attribute !== (metric.key as unknown as Dashboards.Chart['attribute'])
|
||||
)
|
||||
const updated = { ...editableReport, layout: nextLayout }
|
||||
setEditableReport(updated)
|
||||
persistReport(updated)
|
||||
|
||||
if (ref && organization?.slug && removedChart) {
|
||||
sendEvent({
|
||||
action: 'home_custom_report_block_removed',
|
||||
properties: {
|
||||
block_id: String(removedChart.id),
|
||||
},
|
||||
groups: {
|
||||
project: ref,
|
||||
organization: organization.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateChart = (
|
||||
|
||||
@@ -1,18 +1,45 @@
|
||||
import { Check, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Check, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
|
||||
import { cn, Button, Card, CardContent, CardHeader, CardTitle, Badge } from 'ui'
|
||||
|
||||
import { GettingStartedStep } from './GettingStartedSection'
|
||||
import Image from 'next/image'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { Badge, Button, Card, CardContent, cn } from 'ui'
|
||||
import { GettingStartedAction, GettingStartedStep } from './GettingStartedSection'
|
||||
|
||||
// Determine action type for tracking
|
||||
const getActionType = (action: GettingStartedAction): 'primary' | 'ai_assist' | 'external_link' => {
|
||||
// Check if it's an AI assist action (has AiIconAnimation or "Do it for me"/"Generate" labels)
|
||||
if (
|
||||
action.label?.toLowerCase().includes('do it for me') ||
|
||||
action.label?.toLowerCase().includes('generate') ||
|
||||
action.label?.toLowerCase().includes('create policies for me')
|
||||
) {
|
||||
return 'ai_assist'
|
||||
}
|
||||
// Check if it's an external link (href that doesn't start with /project/)
|
||||
if (action.href && !action.href.startsWith('/project/')) {
|
||||
return 'external_link'
|
||||
}
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
export interface GettingStartedProps {
|
||||
steps: GettingStartedStep[]
|
||||
onStepClick: ({
|
||||
stepIndex,
|
||||
stepTitle,
|
||||
actionType,
|
||||
wasCompleted,
|
||||
}: {
|
||||
stepIndex: number
|
||||
stepTitle: string
|
||||
actionType: 'primary' | 'ai_assist' | 'external_link'
|
||||
wasCompleted: boolean
|
||||
}) => void
|
||||
}
|
||||
|
||||
export function GettingStarted({ steps }: GettingStartedProps) {
|
||||
export function GettingStarted({ steps, onStepClick }: GettingStartedProps) {
|
||||
const [activeStepKey, setActiveStepKey] = useState<string | null>(steps[0]?.key ?? null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -172,6 +199,8 @@ export function GettingStarted({ steps }: GettingStartedProps) {
|
||||
return <div key={`${activeStep.key}-action-${i}`}>{action.component}</div>
|
||||
}
|
||||
|
||||
const actionType = getActionType(action)
|
||||
|
||||
if (action.href) {
|
||||
return (
|
||||
<Button
|
||||
@@ -181,7 +210,19 @@ export function GettingStarted({ steps }: GettingStartedProps) {
|
||||
icon={action.icon}
|
||||
className="text-foreground-light hover:text-foreground"
|
||||
>
|
||||
<Link href={action.href}>{action.label}</Link>
|
||||
<Link
|
||||
href={action.href}
|
||||
onClick={() => {
|
||||
onStepClick({
|
||||
stepIndex: activeStepIndex,
|
||||
stepTitle: activeStep.title,
|
||||
actionType,
|
||||
wasCompleted: activeStep.status === 'complete',
|
||||
})
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -191,7 +232,16 @@ export function GettingStarted({ steps }: GettingStartedProps) {
|
||||
key={`${activeStep.key}-action-${i}`}
|
||||
type={action.variant ?? 'default'}
|
||||
icon={action.icon}
|
||||
onClick={action.onClick}
|
||||
onClick={() => {
|
||||
action.onClick?.()
|
||||
onStepClick({
|
||||
stepIndex: activeStepIndex,
|
||||
stepTitle: activeStep.title,
|
||||
actionType,
|
||||
wasCompleted: activeStep.status === 'complete',
|
||||
})
|
||||
}}
|
||||
className="text-foreground-light hover:text-foreground"
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import { useParams } from 'common'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
||||
|
||||
import { GettingStarted } from './GettingStarted'
|
||||
import { FrameworkSelector } from './FrameworkSelector'
|
||||
import {
|
||||
BarChart3,
|
||||
Code,
|
||||
Database,
|
||||
Table,
|
||||
User,
|
||||
Upload,
|
||||
UserPlus,
|
||||
BarChart3,
|
||||
Shield,
|
||||
Table2,
|
||||
GitBranch,
|
||||
Shield,
|
||||
Table,
|
||||
Table2,
|
||||
Upload,
|
||||
User,
|
||||
UserPlus,
|
||||
} from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { FRAMEWORKS } from 'components/interfaces/Connect/Connect.constants'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
||||
import {
|
||||
AiIconAnimation,
|
||||
Button,
|
||||
@@ -30,7 +31,8 @@ import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from 'ui'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { FrameworkSelector } from './FrameworkSelector'
|
||||
import { GettingStarted } from './GettingStarted'
|
||||
|
||||
export type GettingStartedAction = {
|
||||
label: string
|
||||
@@ -61,13 +63,16 @@ export function GettingStartedSection({
|
||||
onChange: (v: GettingStartedState) => void
|
||||
}) {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const { ref } = useParams()
|
||||
const aiSnap = useAiAssistantStateSnapshot()
|
||||
const router = useRouter()
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
|
||||
// Local state for framework selector preview
|
||||
const [selectedFramework, setSelectedFramework] = useState<string>(FRAMEWORKS[0]?.key ?? 'nextjs')
|
||||
const workflow: 'no-code' | 'code' | null = value === 'code' || value === 'no-code' ? value : null
|
||||
const [previousWorkflow, setPreviousWorkflow] = useState<'no-code' | 'code' | null>(null)
|
||||
|
||||
const { data: tablesData } = useTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
@@ -156,7 +161,7 @@ export function GettingStartedSection({
|
||||
status: tablesCount > 0 ? 'complete' : 'incomplete',
|
||||
title: 'Design your database schema',
|
||||
icon: <Database strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
image: '/img/getting-started/declarative-schemas.png',
|
||||
image: `${BASE_PATH}/img/getting-started/declarative-schemas.png`,
|
||||
description:
|
||||
'Next, create a schema file that defines the structure of your database, either following our declarative schema guide or asking the AI assistant to generate one for you.',
|
||||
actions: [
|
||||
@@ -324,7 +329,7 @@ export function GettingStartedSection({
|
||||
status: tablesCount > 0 ? 'complete' : 'incomplete',
|
||||
title: 'Create your first table',
|
||||
icon: <Database strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
image: '/img/getting-started/sample.png',
|
||||
image: `${BASE_PATH}/img/getting-started/sample.png`,
|
||||
description:
|
||||
"To kick off your new project, let's start by creating your very first database table using either the table editor or the AI assistant to shape the structure for you.",
|
||||
actions: [
|
||||
@@ -488,7 +493,24 @@ export function GettingStartedSection({
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={workflow ?? undefined}
|
||||
onValueChange={(v) => v && onChange(v as 'no-code' | 'code')}
|
||||
onValueChange={(v) => {
|
||||
if (v) {
|
||||
const newWorkflow = v as 'no-code' | 'code'
|
||||
setPreviousWorkflow(workflow)
|
||||
onChange(newWorkflow)
|
||||
sendEvent({
|
||||
action: 'home_getting_started_workflow_clicked',
|
||||
properties: {
|
||||
workflow: newWorkflow === 'no-code' ? 'no_code' : 'code',
|
||||
is_switch: previousWorkflow !== null,
|
||||
},
|
||||
groups: {
|
||||
project: project?.ref || '',
|
||||
organization: organization?.slug || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="no-code"
|
||||
@@ -509,7 +531,31 @@ export function GettingStartedSection({
|
||||
No-code
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<Button size="tiny" type="outline" onClick={() => onChange('hidden')}>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
onChange('hidden')
|
||||
if (workflow) {
|
||||
const completedSteps = (workflow === 'code' ? codeSteps : noCodeSteps).filter(
|
||||
(step) => step.status === 'complete'
|
||||
).length
|
||||
const totalSteps = (workflow === 'code' ? codeSteps : noCodeSteps).length
|
||||
sendEvent({
|
||||
action: 'home_getting_started_closed',
|
||||
properties: {
|
||||
workflow: workflow === 'no-code' ? 'no_code' : 'code',
|
||||
steps_completed: completedSteps,
|
||||
total_steps: totalSteps,
|
||||
},
|
||||
groups: {
|
||||
project: project?.ref || '',
|
||||
organization: organization?.slug || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
@@ -545,7 +591,21 @@ export function GettingStartedSection({
|
||||
<Button
|
||||
size="medium"
|
||||
type="outline"
|
||||
onClick={() => onChange('no-code')}
|
||||
onClick={() => {
|
||||
setPreviousWorkflow(workflow)
|
||||
onChange('no-code')
|
||||
sendEvent({
|
||||
action: 'home_getting_started_workflow_clicked',
|
||||
properties: {
|
||||
workflow: 'no_code',
|
||||
is_switch: previousWorkflow !== null,
|
||||
},
|
||||
groups: {
|
||||
project: project?.ref || '',
|
||||
organization: organization?.slug || '',
|
||||
},
|
||||
})
|
||||
}}
|
||||
className="block gap-2 h-auto p-4 md:p-8 max-w-80 text-left justify-start bg-background "
|
||||
>
|
||||
<Table2 size={20} strokeWidth={1.5} className="text-brand" />
|
||||
@@ -559,7 +619,21 @@ export function GettingStartedSection({
|
||||
<Button
|
||||
size="medium"
|
||||
type="outline"
|
||||
onClick={() => onChange('code')}
|
||||
onClick={() => {
|
||||
setPreviousWorkflow(workflow)
|
||||
onChange('code')
|
||||
sendEvent({
|
||||
action: 'home_getting_started_workflow_clicked',
|
||||
properties: {
|
||||
workflow: 'code',
|
||||
is_switch: previousWorkflow !== null,
|
||||
},
|
||||
groups: {
|
||||
project: project?.ref || '',
|
||||
organization: organization?.slug || '',
|
||||
},
|
||||
})
|
||||
}}
|
||||
className="bg-background block gap-2 h-auto p-4 md:p-8 max-w-80 text-left justify-start"
|
||||
>
|
||||
<Code size={20} strokeWidth={1.5} className="text-brand" />
|
||||
@@ -574,7 +648,27 @@ export function GettingStartedSection({
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<GettingStarted steps={steps} />
|
||||
<GettingStarted
|
||||
steps={steps}
|
||||
onStepClick={({ stepIndex, stepTitle, actionType, wasCompleted }) => {
|
||||
if (workflow) {
|
||||
sendEvent({
|
||||
action: 'home_getting_started_step_clicked',
|
||||
properties: {
|
||||
workflow: workflow === 'no-code' ? 'no_code' : 'code',
|
||||
step_number: stepIndex + 1,
|
||||
step_title: stepTitle,
|
||||
action_type: actionType,
|
||||
was_completed: wasCompleted,
|
||||
},
|
||||
groups: {
|
||||
project: project?.ref || '',
|
||||
organization: organization?.slug || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-ki
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { IS_PLATFORM, useParams } from 'common'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { SortableSection } from 'components/interfaces/HomeNew/SortableSection'
|
||||
import { TopSection } from 'components/interfaces/HomeNew/TopSection'
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
@@ -31,6 +32,7 @@ export const HomeV2 = () => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const { data: parentProject } = useProjectByRefQuery(project?.parent_project_ref)
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
|
||||
const hasShownEnableBranchingModalRef = useRef(false)
|
||||
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
|
||||
@@ -69,6 +71,22 @@ export const HomeV2 = () => {
|
||||
const oldIndex = items.indexOf(String(active.id))
|
||||
const newIndex = items.indexOf(String(over.id))
|
||||
if (oldIndex === -1 || newIndex === -1) return items
|
||||
|
||||
if (project?.ref && organization?.slug) {
|
||||
sendEvent({
|
||||
action: 'home_section_rows_moved',
|
||||
properties: {
|
||||
section_moved: String(active.id),
|
||||
old_position: oldIndex,
|
||||
new_position: newIndex,
|
||||
},
|
||||
groups: {
|
||||
project: project.ref,
|
||||
organization: organization.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return arrayMove(items, oldIndex, newIndex)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { Archive, Code, Database, Key, Zap, ChevronDown } from 'lucide-react'
|
||||
import { Archive, ChevronDown, Code, Database, Key, Zap } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo, useState } from 'react'
|
||||
@@ -7,6 +7,7 @@ import { useMemo, useState } from 'react'
|
||||
import { useParams } from 'common'
|
||||
import NoDataPlaceholder from 'components/ui/Charts/NoDataPlaceholder'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import useProjectUsageStats from 'hooks/analytics/useProjectUsageStats'
|
||||
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
@@ -19,14 +20,14 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
TooltipTrigger,
|
||||
DropdownMenuTrigger,
|
||||
Loading,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { Row } from 'ui-patterns'
|
||||
@@ -94,6 +95,7 @@ export const ProjectUsageSection = () => {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const { projectAuthAll: authEnabled, projectStorageAll: storageEnabled } = useIsFeatureEnabled([
|
||||
'project_auth:all',
|
||||
'project_storage:all',
|
||||
@@ -211,7 +213,7 @@ export const ProjectUsageSection = () => {
|
||||
|
||||
const isLoading = services.some((s) => s.stats.isLoading)
|
||||
|
||||
const handleBarClick = (logRoute: string) => (datum: any) => {
|
||||
const handleBarClick = (logRoute: string, serviceKey: ServiceKey) => (datum: any) => {
|
||||
if (!datum?.timestamp) return
|
||||
|
||||
const datumTimestamp = dayjs(datum.timestamp).toISOString()
|
||||
@@ -224,6 +226,20 @@ export const ProjectUsageSection = () => {
|
||||
})
|
||||
|
||||
router.push(`/project/${projectRef}${logRoute}?${queryParams.toString()}`)
|
||||
|
||||
if (projectRef && organization?.slug) {
|
||||
sendEvent({
|
||||
action: 'home_project_usage_chart_clicked',
|
||||
properties: {
|
||||
service_type: serviceKey,
|
||||
bar_timestamp: datum.timestamp,
|
||||
},
|
||||
groups: {
|
||||
project: projectRef,
|
||||
organization: organization.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const enabledServices = services.filter((s) => s.enabled)
|
||||
@@ -350,7 +366,31 @@ export const ProjectUsageSection = () => {
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle className="text-foreground-light">
|
||||
{s.href ? <Link href={s.href}>{s.title}</Link> : s.title}
|
||||
{s.href ? (
|
||||
<Link
|
||||
href={s.href}
|
||||
onClick={() => {
|
||||
if (projectRef && organization?.slug) {
|
||||
sendEvent({
|
||||
action: 'home_project_usage_service_clicked',
|
||||
properties: {
|
||||
service_type: s.key,
|
||||
total_requests: s.total || 0,
|
||||
error_count: s.err || 0,
|
||||
},
|
||||
groups: {
|
||||
project: projectRef,
|
||||
organization: organization.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{s.title}
|
||||
</Link>
|
||||
) : (
|
||||
s.title
|
||||
)}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<span className="text-foreground text-xl">{(s.total || 0).toLocaleString()}</span>
|
||||
@@ -376,10 +416,10 @@ export const ProjectUsageSection = () => {
|
||||
<CardContent className="p-6 pt-4 flex-1 h-full overflow-hidden">
|
||||
<Loading isFullHeight active={isLoading}>
|
||||
<LogsBarChart
|
||||
isFullHeight
|
||||
data={s.data}
|
||||
DateTimeFormat={datetimeFormat}
|
||||
onBarClick={handleBarClick(s.route)}
|
||||
isFullHeight
|
||||
onBarClick={handleBarClick(s.route, s.key)}
|
||||
EmptyState={
|
||||
<NoDataPlaceholder
|
||||
size="small"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import Link from 'next/link'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
|
||||
type SingleStatProps = {
|
||||
icon: ReactNode
|
||||
label: ReactNode
|
||||
@@ -8,9 +12,37 @@ type SingleStatProps = {
|
||||
className?: string
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
trackingProperties?: {
|
||||
stat_type: 'migrations' | 'backups' | 'branches'
|
||||
stat_value: number
|
||||
}
|
||||
}
|
||||
|
||||
export const SingleStat = ({ icon, label, value, className, href, onClick }: SingleStatProps) => {
|
||||
export const SingleStat = ({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
className,
|
||||
href,
|
||||
onClick,
|
||||
trackingProperties,
|
||||
}: SingleStatProps) => {
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
|
||||
const trackActivityStat = () => {
|
||||
if (trackingProperties && project?.ref && organization?.slug) {
|
||||
sendEvent({
|
||||
action: 'home_activity_stat_clicked',
|
||||
properties: trackingProperties,
|
||||
groups: {
|
||||
project: project.ref,
|
||||
organization: organization.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
const content = (
|
||||
<div className={`group flex items-center gap-4 p-0 text-base justify-start ${className || ''}`}>
|
||||
<div className="w-16 h-16 rounded-md bg-surface-75 group-hover:bg-muted border flex items-center justify-center">
|
||||
@@ -27,7 +59,7 @@ export const SingleStat = ({ icon, label, value, className, href, onClick }: Sin
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link className="group block" href={href}>
|
||||
<Link className="group block" href={href} onClick={trackActivityStat}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -3,12 +3,14 @@ import { Home } from 'components/interfaces/Home/Home'
|
||||
import { HomeV2 } from 'components/interfaces/HomeNew/Home'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import { ProjectLayoutWithAuth } from 'components/layouts/ProjectLayout/ProjectLayout'
|
||||
import { usePHFlag } from 'hooks/ui/useFlag'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const HomePage: NextPageWithLayout = () => {
|
||||
const isHomeNew = useFlag('homeNew')
|
||||
const isHomeNewPH = usePHFlag('homeNew')
|
||||
|
||||
if (isHomeNew) {
|
||||
if (isHomeNew && isHomeNewPH) {
|
||||
return <HomeV2 />
|
||||
}
|
||||
return <Home />
|
||||
|
||||
@@ -1422,6 +1422,287 @@ export interface DpaPdfOpenedEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User selected a workflow in the Getting Started section of HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeGettingStartedWorkflowClickedEvent {
|
||||
action: 'home_getting_started_workflow_clicked'
|
||||
properties: {
|
||||
/**
|
||||
* The workflow selected by the user
|
||||
*/
|
||||
workflow: 'code' | 'no_code'
|
||||
/**
|
||||
* Whether this is switching from another workflow
|
||||
*/
|
||||
is_switch: boolean
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked on a step in the Getting Started section of HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeGettingStartedStepClickedEvent {
|
||||
action: 'home_getting_started_step_clicked'
|
||||
properties: {
|
||||
/**
|
||||
* The workflow type (code or no-code)
|
||||
*/
|
||||
workflow: 'code' | 'no_code'
|
||||
/**
|
||||
* The step number (1-based index)
|
||||
*/
|
||||
step_number: number
|
||||
/**
|
||||
* The title of the step
|
||||
*/
|
||||
step_title: string
|
||||
/**
|
||||
* The action type of the button clicked
|
||||
*/
|
||||
action_type: 'primary' | 'ai_assist' | 'external_link'
|
||||
/**
|
||||
* Whether the step was already completed
|
||||
*/
|
||||
was_completed: boolean
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked on an activity stat in HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeActivityStatClickedEvent {
|
||||
action: 'home_activity_stat_clicked'
|
||||
properties: {
|
||||
/**
|
||||
* The type of activity stat clicked
|
||||
*/
|
||||
stat_type: 'migrations' | 'backups' | 'branches'
|
||||
/**
|
||||
* The current value of the stat
|
||||
*/
|
||||
stat_value: number
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked the main Ask Assistant button in the Advisor section of HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeAdvisorAskAssistantClickedEvent {
|
||||
action: 'home_advisor_ask_assistant_clicked'
|
||||
properties: {
|
||||
/**
|
||||
* Number of issues found by the advisor
|
||||
*/
|
||||
issues_count: number
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked on an issue card in the Advisor section of HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeAdvisorIssueCardClickedEvent {
|
||||
action: 'home_advisor_issue_card_clicked'
|
||||
properties: {
|
||||
/**
|
||||
* Category of the issue (SECURITY or PERFORMANCE)
|
||||
*/
|
||||
issue_category: string
|
||||
/**
|
||||
* Name/key of the lint issue
|
||||
*/
|
||||
issue_name: string
|
||||
issues_count: number
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked the Fix Issue button on an advisor card in HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeAdvisorFixIssueClickedEvent {
|
||||
action: 'home_advisor_fix_issue_clicked'
|
||||
properties: {
|
||||
/**
|
||||
* Category of the issue (SECURITY or PERFORMANCE)
|
||||
*/
|
||||
issue_category: string
|
||||
/**
|
||||
* Name/key of the lint issue
|
||||
*/
|
||||
issue_name: string
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked on a service title in Project Usage section of HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeProjectUsageServiceClickedEvent {
|
||||
action: 'home_project_usage_service_clicked'
|
||||
properties: {
|
||||
/**
|
||||
* The service that was clicked
|
||||
*/
|
||||
service_type: 'db' | 'functions' | 'auth' | 'storage' | 'realtime'
|
||||
/**
|
||||
* Total requests for this service
|
||||
*/
|
||||
total_requests: number
|
||||
/**
|
||||
* Number of errors for this service
|
||||
*/
|
||||
error_count: number
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked on a bar in the usage chart in HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeProjectUsageChartClickedEvent {
|
||||
action: 'home_project_usage_chart_clicked'
|
||||
properties: {
|
||||
/**
|
||||
* The service type for this chart
|
||||
*/
|
||||
service_type: 'db' | 'functions' | 'auth' | 'storage' | 'realtime'
|
||||
/**
|
||||
* Timestamp of the bar clicked
|
||||
*/
|
||||
bar_timestamp: string
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User added a block to the custom report in HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeCustomReportBlockAddedEvent {
|
||||
action: 'home_custom_report_block_added'
|
||||
properties: {
|
||||
/**
|
||||
* ID of the snippet/block added
|
||||
*/
|
||||
block_id: string
|
||||
/**
|
||||
* If position is 0 it is equivalent to 'Add your first chart'.
|
||||
*/
|
||||
position: number
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User removed a block from the custom report in HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeCustomReportBlockRemovedEvent {
|
||||
action: 'home_custom_report_block_removed'
|
||||
properties: {
|
||||
/**
|
||||
* ID of the block removed
|
||||
*/
|
||||
block_id: string
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User dismissed the Getting Started section in HomeV2.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeGettingStartedClosedEvent {
|
||||
action: 'home_getting_started_closed'
|
||||
properties: {
|
||||
/**
|
||||
* The current workflow when dismissed
|
||||
*/
|
||||
workflow: 'code' | 'no_code'
|
||||
/**
|
||||
* Number of steps completed when dismissed
|
||||
*/
|
||||
steps_completed: number
|
||||
/**
|
||||
* Total number of steps in the workflow
|
||||
*/
|
||||
total_steps: number
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User reordered sections in HomeV2 using drag and drop.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}
|
||||
*/
|
||||
export interface HomeSectionRowsMovedEvent {
|
||||
action: 'home_section_rows_moved'
|
||||
properties: {
|
||||
/**
|
||||
* The section that was moved
|
||||
*/
|
||||
section_moved: string
|
||||
/**
|
||||
* The old position of the section (0-based index)
|
||||
*/
|
||||
old_position: number
|
||||
/**
|
||||
* The new position of the section (0-based index)
|
||||
*/
|
||||
new_position: number
|
||||
}
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked the Request DPA button to open the confirmation modal.
|
||||
*
|
||||
@@ -1555,6 +1836,18 @@ export type TelemetryEvent =
|
||||
| BranchUpdatedEvent
|
||||
| BranchReviewWithAssistantClickedEvent
|
||||
| DpaPdfOpenedEvent
|
||||
| HomeGettingStartedWorkflowClickedEvent
|
||||
| HomeGettingStartedStepClickedEvent
|
||||
| HomeGettingStartedClosedEvent
|
||||
| HomeSectionRowsMovedEvent
|
||||
| HomeActivityStatClickedEvent
|
||||
| HomeAdvisorAskAssistantClickedEvent
|
||||
| HomeAdvisorIssueCardClickedEvent
|
||||
| HomeAdvisorFixIssueClickedEvent
|
||||
| HomeProjectUsageServiceClickedEvent
|
||||
| HomeProjectUsageChartClickedEvent
|
||||
| HomeCustomReportBlockAddedEvent
|
||||
| HomeCustomReportBlockRemovedEvent
|
||||
| DpaRequestButtonClickedEvent
|
||||
| DocumentViewButtonClickedEvent
|
||||
| HipaaRequestButtonClickedEvent
|
||||
|
||||
Reference in New Issue
Block a user