Files
supabase/apps/studio/components/interfaces/QueryPerformance/WithStatements/WithStatements.tsx
T
kemal.earth 70a64f8c00 feat(studio): query performance metrics chart (#39431)
* feat: setup chart area and tabs

This sets up the area where we can expect the insights chart as well as the tabs mechanism.

* feat: parse pg_stat_monitor logs as json

* feat: create query perf chart utils and move transfrom function

Created a utils file for our QueryPerformanceChart component. This moves the logs to JSON transform function there.

* feat: add timerange to chart

* feat: add date selector to query perf overview

This adds the selector to the top right of the page allowing the user to switch between last hour, 3 hours and 24 hours

* feat: modify chart component to accomodate hiding bits

* feat: add metrics to each tab

* chore: update to 60 min by default and some css

* feat: centralise data parsing for logs

* feat: clean up filters bar

This rewires the export to give you the aggregate pg_stat_monitor data. Also removes unused buttons and filters.

* feat: percentiles for query latency chart

* feat: filter out non evenets from pg_stat_monitor logs

* feat: utils for cache misses and hits

* feat: add selected query to chart on click

* feat: add click through to query panel

* chore: tidy up files

* chore: distinction between selected and open panel

* feat: move query performance fully into reports area

* fix: preserve query params on reports link

* fix: remove right icon syntax in report menu

* chore: remove cache misses from cache chart

* refactor: backwards compatibility for statements if right db version isnt available

* chore: delete randomly generated empty file

* chore: tidy up unused imports and vars

* chore: remove console logs

* chore: remove isMounted from query perf

* fix: cmd k query perf path

* feat: simplify query latency only p50 and p95

This seems to give us a more accurate reading as we can calculate these two

* fix: cache hit rate not showing inside query details

* chore: chart bg colour adjust

So it contrasts a little better on light mode.

* feat: show selected query on other verticals

* feat: bring back symlink in advisors
2025-10-15 13:39:29 +01:00

187 lines
6.5 KiB
TypeScript

import { LoadingLine, cn } from 'ui'
import { useState, useEffect, useMemo } from 'react'
import { Button } from 'ui'
import { X, RefreshCw, RotateCcw } from 'lucide-react'
import { Markdown } from '../../Markdown'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL, IS_PLATFORM } from 'lib/constants'
import { executeSql } from 'data/sql/execute-sql-query'
import { toast } from 'sonner'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
import { PresetHookResult } from 'components/interfaces/Reports/Reports.utils'
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
import { QueryPerformanceMetrics } from '../QueryPerformanceMetrics'
import { QueryPerformanceFilterBar } from '../QueryPerformanceFilterBar'
import { QueryPerformanceGrid } from '../QueryPerformanceGrid'
import { transformStatementDataToRows } from './WithStatements.utils'
import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
interface WithStatementsProps {
queryHitRate: PresetHookResult
queryPerformanceQuery: DbQueryHook<any>
queryMetrics: PresetHookResult
}
export const WithStatements = ({
queryHitRate,
queryPerformanceQuery,
queryMetrics,
}: WithStatementsProps) => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const state = useDatabaseSelectorStateSnapshot()
const { data, isLoading, isRefetching } = queryPerformanceQuery
const isPrimaryDatabase = state.selectedDatabaseId === ref
const formattedDatabaseId = formatDatabaseID(state.selectedDatabaseId ?? '')
const [showResetgPgStatStatements, setShowResetgPgStatStatements] = useState(false)
const [showBottomSection, setShowBottomSection] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.QUERY_PERF_SHOW_BOTTOM_SECTION,
true
)
const handleRefresh = () => {
queryPerformanceQuery.runQuery()
queryHitRate.runQuery()
queryMetrics.runQuery()
}
const processedData = useMemo(() => {
return transformStatementDataToRows(data || [])
}, [data])
const { data: databases } = useReadReplicasQuery({ projectRef: ref })
useEffect(() => {
state.setSelectedDatabaseId(ref)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref])
return (
<>
<QueryPerformanceMetrics />
<QueryPerformanceFilterBar
actions={
<>
<Button
type="default"
size="tiny"
icon={<RefreshCw />}
onClick={handleRefresh}
loading={isRefetching}
>
Refresh
</Button>
<Button
type="default"
size="tiny"
icon={<RotateCcw />}
onClick={() => setShowResetgPgStatStatements(true)}
>
Reset report
</Button>
<DownloadResultsButton
results={processedData}
fileName={`Supabase Query Performance Statements (${ref})`}
align="end"
/>
</>
}
/>
<LoadingLine loading={isLoading || isRefetching} />
<QueryPerformanceGrid aggregatedData={processedData} isLoading={isLoading} />
<div
className={cn('px-6 py-6 flex gap-x-4 border-t relative', {
hidden: showBottomSection === false,
})}
>
<Button
className="absolute top-1.5 right-3 px-1.5"
type="text"
size="tiny"
onClick={() => setShowBottomSection(false)}
>
<X size="14" />
</Button>
<div className="w-[33%] flex flex-col gap-y-1 text-sm">
<p>Reset report</p>
<p className="text-xs text-foreground-light">
Consider resetting the analysis after optimizing any queries
</p>
<Button
type="default"
className="!mt-3 w-min"
onClick={() => setShowResetgPgStatStatements(true)}
>
Reset report
</Button>
</div>
<div className="w-[33%] flex flex-col gap-y-1 text-sm">
<p>How is this report generated?</p>
<Markdown
className="text-xs"
content={`This report uses the pg_stat_statements table, and pg_stat_statements extension. [Learn more here](${DOCS_URL}/guides/platform/performance#examining-query-performance).`}
/>
</div>
<div className="w-[33%] flex flex-col gap-y-1 text-sm">
<p>Inspect your database for potential issues</p>
<Markdown
className="text-xs"
content={`The Supabase CLI comes with a range of tools to help inspect your Postgres instances for
potential issues. [Learn more here](${DOCS_URL}/guides/database/inspect).`}
/>
</div>
</div>
<ConfirmationModal
visible={showResetgPgStatStatements}
size="medium"
variant="destructive"
title="Reset query performance analysis"
confirmLabel="Reset report"
confirmLabelLoading="Resetting report"
onCancel={() => setShowResetgPgStatStatements(false)}
onConfirm={async () => {
const connectionString = databases?.find(
(db) => db.identifier === state.selectedDatabaseId
)?.connectionString
if (IS_PLATFORM && !connectionString) {
return toast.error('Unable to run query: Connection string is missing')
}
try {
await executeSql({
projectRef: project?.ref,
connectionString,
sql: `SELECT pg_stat_statements_reset();`,
})
handleRefresh()
setShowResetgPgStatStatements(false)
} catch (error: any) {
toast.error(`Failed to reset analysis: ${error.message}`)
}
}}
>
<p className="text-foreground-light text-sm">
This will reset the pg_stat_statements table in the extensions schema on your{' '}
<span className="text-foreground">
{isPrimaryDatabase ? 'primary database' : `read replica (ID: ${formattedDatabaseId})`}
</span>
, which is used to calculate query performance. This data will repopulate immediately
after.
</p>
</ConfirmationModal>
</>
)
}