OverviewMetrics.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import { useQuery } from '@tanstack/react-query'
  2. import { useParams } from 'common'
  3. import dayjs from 'dayjs'
  4. import { BarChart2, ChevronRight, ExternalLink, Telescope } from 'lucide-react'
  5. import Link from 'next/link'
  6. import { useRouter } from 'next/router'
  7. import { AiIconAnimation, Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  8. import { StatusCode } from 'ui-patterns'
  9. import {
  10. Chart,
  11. ChartActions,
  12. ChartCard,
  13. ChartContent,
  14. ChartEmptyState,
  15. ChartHeader,
  16. ChartLoadingState,
  17. ChartMetric,
  18. ChartTitle,
  19. } from 'ui-patterns/Chart'
  20. import {
  21. PageSection,
  22. PageSectionContent,
  23. PageSectionMeta,
  24. PageSectionSummary,
  25. PageSectionTitle,
  26. } from 'ui-patterns/PageSection'
  27. import {
  28. AuthErrorCodeRow,
  29. fetchTopAuthErrorCodes,
  30. fetchTopResponseErrors,
  31. ResponseErrorRow,
  32. } from './OverviewErrors.constants'
  33. import { OverviewTable } from './OverviewTable'
  34. import {
  35. AuthMetricsResponse,
  36. calculatePercentageChange,
  37. getApiSuccessRates,
  38. getAuthSuccessRates,
  39. getMetricValues,
  40. } from './OverviewUsage.constants'
  41. import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
  42. import AlertError from '@/components/ui/AlertError'
  43. import { ErrorCodeTooltip } from '@/components/ui/ErrorCodeTooltip/ErrorCodeTooltip'
  44. import { Service } from '@/data/graphql/graphql'
  45. import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
  46. import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
  47. const StatCard = ({
  48. title,
  49. current,
  50. previous,
  51. loading,
  52. suffix = '',
  53. href,
  54. tooltip,
  55. }: {
  56. title: string
  57. current: number
  58. previous: number
  59. loading: boolean
  60. suffix?: string
  61. invert?: boolean
  62. href?: string
  63. tooltip?: string
  64. }) => {
  65. const router = useRouter()
  66. const formattedCurrent =
  67. suffix === 'ms'
  68. ? current.toFixed(2)
  69. : suffix === '%'
  70. ? current.toFixed(1)
  71. : Math.round(current).toLocaleString()
  72. // const signChar = previous > 0 ? '+' : previous < 0 ? '-' : ''
  73. const actions = [
  74. {
  75. label: 'Go to Auth Report',
  76. icon: <ExternalLink size={12} />,
  77. onClick: href ? () => router.push(href) : undefined,
  78. },
  79. ]
  80. return (
  81. <Chart isLoading={loading}>
  82. <ChartCard>
  83. <ChartHeader align="start">
  84. <ChartMetric
  85. className="pb-4"
  86. label={title}
  87. tooltip={tooltip}
  88. diffValue={`${previous.toFixed(1)}%`}
  89. value={`${formattedCurrent}${suffix}`}
  90. />
  91. <ChartActions actions={actions} />
  92. </ChartHeader>
  93. </ChartCard>
  94. </Chart>
  95. )
  96. }
  97. const LogsLink = ({ href }: { href: string }) => (
  98. <Tooltip>
  99. <TooltipTrigger asChild>
  100. <Button
  101. type="text"
  102. size="tiny"
  103. className="p-1.5 text-foreground-lighter hover:text-foreground"
  104. asChild
  105. >
  106. <Link href={href} aria-label="Go to Logs">
  107. <ChevronRight size={12} />
  108. </Link>
  109. </Button>
  110. </TooltipTrigger>
  111. <TooltipContent>Go to Logs</TooltipContent>
  112. </Tooltip>
  113. )
  114. function isResponseErrorRow(row: unknown): row is ResponseErrorRow {
  115. if (!row || typeof row !== 'object') return false
  116. const r = row as Record<string, unknown>
  117. return (
  118. typeof r.method === 'string' &&
  119. typeof r.path === 'string' &&
  120. typeof r.status_code === 'number' &&
  121. typeof r.count === 'number'
  122. )
  123. }
  124. function isAuthErrorCodeRow(row: unknown): row is AuthErrorCodeRow {
  125. if (!row || typeof row !== 'object') return false
  126. const r = row as Record<string, unknown>
  127. return typeof r.error_code === 'string' && typeof r.count === 'number'
  128. }
  129. interface OverviewMetricsProps {
  130. metrics?: AuthMetricsResponse
  131. isLoading: boolean
  132. error: unknown
  133. }
  134. export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsProps) => {
  135. const { ref } = useParams()
  136. const endDate = dayjs().toISOString()
  137. const startDate = dayjs().subtract(24, 'hour').toISOString()
  138. const aiSnap = useAiAssistantStateSnapshot()
  139. const { openSidebar } = useSidebarManagerSnapshot()
  140. const { current: activeUsersCurrent, previous: activeUsersPrevious } = getMetricValues(
  141. metrics,
  142. 'activeUsers'
  143. )
  144. const { current: signUpsCurrent, previous: signUpsPrevious } = getMetricValues(
  145. metrics,
  146. 'signUpCount'
  147. )
  148. const activeUsersChange = calculatePercentageChange(activeUsersCurrent, activeUsersPrevious)
  149. const signUpsChange = calculatePercentageChange(signUpsCurrent, signUpsPrevious)
  150. const { current: apiSuccessRateCurrent, previous: apiSuccessRatePrevious } =
  151. getApiSuccessRates(metrics)
  152. const { current: authSuccessRateCurrent, previous: authSuccessRatePrevious } =
  153. getAuthSuccessRates(metrics)
  154. const apiSuccessRateChange = calculatePercentageChange(
  155. apiSuccessRateCurrent,
  156. apiSuccessRatePrevious
  157. )
  158. const authSuccessRateChange = calculatePercentageChange(
  159. authSuccessRateCurrent,
  160. authSuccessRatePrevious
  161. )
  162. const { data: respErrData, isPending: isLoadingResp } = useQuery({
  163. queryKey: ['auth-overview', ref, 'top-response-errors'],
  164. queryFn: () => fetchTopResponseErrors(ref as string),
  165. enabled: !!ref,
  166. })
  167. const { data: codeErrData, isPending: isLoadingCodes } = useQuery({
  168. queryKey: ['auth-overview', ref, 'top-auth-error-codes'],
  169. queryFn: () => fetchTopAuthErrorCodes(ref as string),
  170. enabled: !!ref,
  171. })
  172. const responseErrors: ResponseErrorRow[] = Array.isArray(respErrData?.result)
  173. ? (respErrData?.result as unknown[]).filter(isResponseErrorRow)
  174. : []
  175. const errorCodes: AuthErrorCodeRow[] = Array.isArray(codeErrData?.result)
  176. ? (codeErrData?.result as unknown[]).filter(isAuthErrorCodeRow)
  177. : []
  178. const errorCodesActions = [
  179. {
  180. label: 'Ask Assistant about Error Codes',
  181. icon: <AiIconAnimation size={12} />,
  182. onClick: () => {
  183. openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
  184. aiSnap.newChat({
  185. name: 'Auth Help',
  186. initialInput: `Can you explain to me what the authentication error codes mean?`,
  187. })
  188. },
  189. },
  190. ]
  191. return (
  192. <>
  193. <PageSection>
  194. {!!error && (
  195. <AlertError
  196. className="mb-4"
  197. subject="Error fetching auth metrics"
  198. error={{
  199. message: 'There was an error fetching the auth metrics.',
  200. }}
  201. />
  202. )}
  203. <PageSectionMeta>
  204. <PageSectionSummary>
  205. <div className="flex items-center justify-between">
  206. <PageSectionTitle>Usage</PageSectionTitle>
  207. <Link
  208. href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}&isHelper=true&helperText=Last+24+hours`}
  209. className="text-foreground underline underline-offset-2 decoration-foreground-muted hover:decoration-foreground transition-all text-sm inline-flex items-center gap-x-1.5"
  210. >
  211. <Telescope size={14} className="text-foreground-lighter" />
  212. <span>Go to observability</span>
  213. <ChevronRight size={14} className="text-foreground-lighter" />
  214. </Link>
  215. </div>
  216. </PageSectionSummary>
  217. </PageSectionMeta>
  218. <PageSectionContent>
  219. <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
  220. <StatCard
  221. title="Auth Activity"
  222. current={activeUsersCurrent}
  223. previous={activeUsersChange}
  224. loading={isLoading}
  225. href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
  226. tooltip="Users who generated any Auth event in this period. This metric tracks authentication activity, not total product usage. Some active users won't appear here if their session stayed valid."
  227. />
  228. <StatCard
  229. title="Sign ups"
  230. current={signUpsCurrent}
  231. previous={signUpsChange}
  232. loading={isLoading}
  233. href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
  234. />
  235. </div>
  236. </PageSectionContent>
  237. </PageSection>
  238. <PageSection>
  239. <PageSectionMeta>
  240. <PageSectionSummary>
  241. <PageSectionTitle>Monitoring</PageSectionTitle>
  242. </PageSectionSummary>
  243. </PageSectionMeta>
  244. <PageSectionContent>
  245. <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
  246. <StatCard
  247. title="Auth API Success Rate"
  248. current={apiSuccessRateCurrent}
  249. previous={apiSuccessRateChange}
  250. loading={isLoading}
  251. suffix="%"
  252. href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
  253. />
  254. <StatCard
  255. title="Auth Server Success Rate"
  256. current={authSuccessRateCurrent}
  257. previous={authSuccessRateChange}
  258. loading={isLoading}
  259. suffix="%"
  260. href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#monitoring`}
  261. />
  262. </div>
  263. <div className="grid grid-cols-1 gap-4">
  264. <Chart isLoading={isLoadingResp}>
  265. <ChartCard>
  266. <ChartHeader>
  267. <ChartTitle>Auth API Errors</ChartTitle>
  268. </ChartHeader>
  269. <ChartContent
  270. className="p-0!"
  271. isEmpty={responseErrors.length === 0}
  272. emptyState={
  273. <div className="p-6">
  274. <ChartEmptyState
  275. icon={<BarChart2 size={16} />}
  276. title="No data to show"
  277. description="It may take up to 24 hours for data to refresh"
  278. />
  279. </div>
  280. }
  281. loadingState={
  282. <div className="p-6">
  283. <ChartLoadingState />
  284. </div>
  285. }
  286. >
  287. <OverviewTable<ResponseErrorRow>
  288. isLoading={isLoadingResp}
  289. data={responseErrors}
  290. columns={[
  291. {
  292. key: 'request',
  293. header: 'Request',
  294. className: 'w-auto pr-0!',
  295. render: (row) => {
  296. return <StatusCode method={row.method} statusCode={row.status_code} />
  297. },
  298. },
  299. {
  300. key: 'path',
  301. header: 'Path',
  302. className: 'w-full',
  303. render: (row) => (
  304. <span className="line-clamp-1 font-mono text-foreground-light text-xs">
  305. {row.path}
  306. </span>
  307. ),
  308. },
  309. {
  310. key: 'count',
  311. header: 'Count',
  312. className: 'text-right shrink-0 ml-auto justify-end',
  313. render: (row) => (
  314. <div className="flex justify-end items-center gap-2">
  315. <div className="text-right text-xs tabular-nums">{row.count}</div>
  316. <LogsLink href={`/project/${ref}/logs/edge-logs?s=${row.path}`} />
  317. </div>
  318. ),
  319. },
  320. ]}
  321. />
  322. </ChartContent>
  323. </ChartCard>
  324. </Chart>
  325. <Chart isLoading={isLoadingCodes}>
  326. <ChartCard>
  327. <ChartHeader>
  328. <ChartTitle>Auth Server Errors</ChartTitle>
  329. <ChartActions actions={errorCodesActions} />
  330. </ChartHeader>
  331. <ChartContent
  332. className="p-0!"
  333. isEmpty={errorCodes.length === 0}
  334. emptyState={
  335. <div className="p-6">
  336. <ChartEmptyState
  337. icon={<BarChart2 size={16} />}
  338. title="No data to show"
  339. description="It may take up to 24 hours for data to refresh"
  340. />
  341. </div>
  342. }
  343. loadingState={
  344. <div className="p-6">
  345. <ChartLoadingState />
  346. </div>
  347. }
  348. >
  349. <OverviewTable<AuthErrorCodeRow>
  350. isLoading={isLoadingCodes}
  351. data={errorCodes}
  352. columns={[
  353. {
  354. key: 'error_code',
  355. header: 'Error code',
  356. className: 'w-full',
  357. render: (row) => (
  358. <ErrorCodeTooltip errorCode={row.error_code} service={Service.Auth}>
  359. <span className="line-clamp-1 font-mono uppercase text-xs inline-flex text-foreground-light cursor-default">
  360. {row.error_code}
  361. </span>
  362. </ErrorCodeTooltip>
  363. ),
  364. },
  365. {
  366. key: 'count',
  367. header: 'Count',
  368. className: 'text-right',
  369. render: (row) => (
  370. <div className="flex justify-end items-center gap-2">
  371. <div className="text-right text-xs tabular-nums">{row.count}</div>
  372. <LogsLink href={`/project/${ref}/logs/auth-logs?s=${row.error_code}`} />
  373. </div>
  374. ),
  375. },
  376. ]}
  377. />
  378. </ChartContent>
  379. </ChartCard>
  380. </Chart>
  381. </div>
  382. </PageSectionContent>
  383. </PageSection>
  384. </>
  385. )
  386. }