auth.tsx369 lines · main
| 1 | import { useParams } from 'common' |
| 2 | import dayjs from 'dayjs' |
| 3 | import { ArrowRight, LogsIcon, RefreshCw } from 'lucide-react' |
| 4 | import { useRouter } from 'next/router' |
| 5 | import { parseAsJson, useQueryState } from 'nuqs' |
| 6 | import { useState } from 'react' |
| 7 | |
| 8 | import ReportFilterBar from '@/components/interfaces/Reports/ReportFilterBar' |
| 9 | import ReportHeader from '@/components/interfaces/Reports/ReportHeader' |
| 10 | import ReportPadding from '@/components/interfaces/Reports/ReportPadding' |
| 11 | import { REPORT_DATERANGE_HELPER_LABELS } from '@/components/interfaces/Reports/Reports.constants' |
| 12 | import ReportStickyNav from '@/components/interfaces/Reports/ReportStickyNav' |
| 13 | import { SharedAPIReport } from '@/components/interfaces/Reports/SharedAPIReport/SharedAPIReport' |
| 14 | import { useSharedAPIReport } from '@/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants' |
| 15 | import { ReportChartV2 } from '@/components/interfaces/Reports/v2/ReportChartV2' |
| 16 | import { ReportSectionHeader } from '@/components/interfaces/Reports/v2/ReportSectionHeader' |
| 17 | import { |
| 18 | numericFilterSchema, |
| 19 | ReportsNumericFilter, |
| 20 | } from '@/components/interfaces/Reports/v2/ReportsNumericFilter' |
| 21 | import { |
| 22 | ReportsSelectFilter, |
| 23 | selectFilterSchema, |
| 24 | } from '@/components/interfaces/Reports/v2/ReportsSelectFilter' |
| 25 | import { LogsDatePicker } from '@/components/interfaces/Settings/Logs/Logs.DatePickers' |
| 26 | import UpgradePrompt from '@/components/interfaces/Settings/Logs/UpgradePrompt' |
| 27 | import DefaultLayout from '@/components/layouts/DefaultLayout' |
| 28 | import ObservabilityLayout from '@/components/layouts/ObservabilityLayout/ObservabilityLayout' |
| 29 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 30 | import type { ChartHighlightAction } from '@/components/ui/Charts/ChartHighlightActions' |
| 31 | import { ReportSettings } from '@/components/ui/Charts/ReportSettings' |
| 32 | import { ObservabilityLink } from '@/components/ui/ObservabilityLink' |
| 33 | import { |
| 34 | createErrorsReportConfig, |
| 35 | createLatencyReportConfig, |
| 36 | createUsageReportConfig, |
| 37 | } from '@/data/reports/v2/auth.config' |
| 38 | import { useRefreshHandler, useReportDateRange } from '@/hooks/misc/useReportDateRange' |
| 39 | import type { NextPageWithLayout } from '@/types' |
| 40 | |
| 41 | const AuthReport: NextPageWithLayout = () => { |
| 42 | return ( |
| 43 | <ReportPadding> |
| 44 | <AuthUsage /> |
| 45 | </ReportPadding> |
| 46 | ) |
| 47 | } |
| 48 | |
| 49 | AuthReport.getLayout = (page) => ( |
| 50 | <DefaultLayout> |
| 51 | <ObservabilityLayout title="Auth">{page}</ObservabilityLayout> |
| 52 | </DefaultLayout> |
| 53 | ) |
| 54 | |
| 55 | export type UpdateDateRange = (from: string, to: string) => void |
| 56 | export default AuthReport |
| 57 | |
| 58 | const AuthUsage = () => { |
| 59 | const { ref } = useParams() |
| 60 | const chartSyncId = `auth-report` |
| 61 | |
| 62 | const { |
| 63 | selectedDateRange, |
| 64 | updateDateRange, |
| 65 | datePickerValue, |
| 66 | datePickerHelpers, |
| 67 | showUpgradePrompt, |
| 68 | setShowUpgradePrompt, |
| 69 | handleDatePickerChange, |
| 70 | } = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES) |
| 71 | |
| 72 | const { |
| 73 | data, |
| 74 | error, |
| 75 | isLoading, |
| 76 | refetch, |
| 77 | isRefetching, |
| 78 | filters, |
| 79 | addFilter, |
| 80 | removeFilters, |
| 81 | isLoadingData, |
| 82 | sql, |
| 83 | } = useSharedAPIReport({ |
| 84 | filterBy: 'auth', |
| 85 | start: selectedDateRange?.period_start?.date, |
| 86 | end: selectedDateRange?.period_end?.date, |
| 87 | }) |
| 88 | |
| 89 | const [isRefreshing, setIsRefreshing] = useState(false) |
| 90 | |
| 91 | const [monitoringStatusCodeFilter, setMonitoringStatusCodeFilter] = useQueryState( |
| 92 | 'monitoring_status_code', |
| 93 | parseAsJson(numericFilterSchema.parse) |
| 94 | ) |
| 95 | |
| 96 | const [usageProviderFilter, setUsageProviderFilter] = useQueryState( |
| 97 | 'usage_provider', |
| 98 | parseAsJson(selectFilterSchema.parse) |
| 99 | ) |
| 100 | |
| 101 | const providerOptions = [ |
| 102 | { label: 'Email', value: 'email' }, |
| 103 | { label: 'Google', value: 'google' }, |
| 104 | { label: 'Apple', value: 'apple' }, |
| 105 | { label: 'Phone', value: 'phone' }, |
| 106 | { label: 'Discord', value: 'discord' }, |
| 107 | { label: 'Azure', value: 'azure' }, |
| 108 | { label: 'GitHub', value: 'github' }, |
| 109 | { label: 'Kakao', value: 'kakao' }, |
| 110 | { label: 'TOTP', value: 'totp' }, |
| 111 | { label: 'Twitter', value: 'twitter' }, |
| 112 | { label: 'SAML', value: 'saml' }, |
| 113 | { label: 'Recovery', value: 'recovery' }, |
| 114 | { label: 'SSO/SAML', value: 'sso/saml' }, |
| 115 | { label: 'Magic Link', value: 'magiclink' }, |
| 116 | { label: 'Keycloak', value: 'keycloak' }, |
| 117 | { label: 'Facebook', value: 'facebook' }, |
| 118 | { label: 'Twitch', value: 'twitch' }, |
| 119 | { label: 'LinkedIn OIDC', value: 'linkedin_oidc' }, |
| 120 | { label: 'Spotify', value: 'spotify' }, |
| 121 | { label: 'Email Change', value: 'email_change' }, |
| 122 | { label: 'Snapchat', value: 'snapchat' }, |
| 123 | { label: 'WorkOS', value: 'workos' }, |
| 124 | { label: 'Notion', value: 'notion' }, |
| 125 | { label: 'Slack OIDC', value: 'slack_oidc' }, |
| 126 | { label: 'Figma', value: 'figma' }, |
| 127 | { label: 'GitLab', value: 'gitlab' }, |
| 128 | { label: 'LinkedIn', value: 'linkedin' }, |
| 129 | { label: 'Zoom', value: 'zoom' }, |
| 130 | ] |
| 131 | |
| 132 | const usageReportConfig = createUsageReportConfig({ |
| 133 | projectRef: ref || '', |
| 134 | startDate: selectedDateRange?.period_start?.date, |
| 135 | endDate: selectedDateRange?.period_end?.date, |
| 136 | interval: selectedDateRange?.interval, |
| 137 | filters: { provider: usageProviderFilter }, |
| 138 | }) |
| 139 | |
| 140 | const errorsReportConfig = createErrorsReportConfig({ |
| 141 | projectRef: ref || '', |
| 142 | startDate: selectedDateRange?.period_start?.date, |
| 143 | endDate: selectedDateRange?.period_end?.date, |
| 144 | interval: selectedDateRange?.interval, |
| 145 | filters: { status_code: monitoringStatusCodeFilter }, |
| 146 | }) |
| 147 | |
| 148 | const latencyReportConfig = createLatencyReportConfig({ |
| 149 | projectRef: ref || '', |
| 150 | startDate: selectedDateRange?.period_start?.date, |
| 151 | endDate: selectedDateRange?.period_end?.date, |
| 152 | interval: selectedDateRange?.interval, |
| 153 | filters: {}, |
| 154 | }) |
| 155 | |
| 156 | const onRefreshReport = useRefreshHandler( |
| 157 | datePickerValue, |
| 158 | datePickerHelpers, |
| 159 | handleDatePickerChange, |
| 160 | async () => { |
| 161 | if (!selectedDateRange) return |
| 162 | |
| 163 | setIsRefreshing(true) |
| 164 | refetch() |
| 165 | setTimeout(() => setIsRefreshing(false), 1000) |
| 166 | } |
| 167 | ) |
| 168 | |
| 169 | const router = useRouter() |
| 170 | |
| 171 | const highlightActions: ChartHighlightAction[] = [ |
| 172 | { |
| 173 | id: 'api-gateway-logs', |
| 174 | label: 'Open in API Gateway Logs', |
| 175 | icon: <LogsIcon size={12} />, |
| 176 | onSelect: ({ start, end, chartId }) => { |
| 177 | let url = `/project/${ref}/logs/edge-logs?its=${start}&ite=${end}` |
| 178 | |
| 179 | if (chartId?.includes('errors')) { |
| 180 | url += `&f={"product":{"auth":true},"status_code":{"error":true,"warning":true}}` |
| 181 | } else { |
| 182 | url += `&f={"product":{"auth":true}}` |
| 183 | } |
| 184 | |
| 185 | router.push(url) |
| 186 | }, |
| 187 | }, |
| 188 | { |
| 189 | id: 'auth-logs', |
| 190 | label: 'Open in Auth Logs', |
| 191 | icon: <LogsIcon size={12} />, |
| 192 | onSelect: ({ start, end }) => { |
| 193 | const url = `/project/${ref}/logs/auth-logs?its=${start}&ite=${end}` |
| 194 | router.push(url) |
| 195 | }, |
| 196 | }, |
| 197 | ] |
| 198 | |
| 199 | return ( |
| 200 | <> |
| 201 | <ReportHeader title="Auth" showDatabaseSelector={false} /> |
| 202 | <ReportStickyNav |
| 203 | content={ |
| 204 | <div className="flex flex-col gap-2"> |
| 205 | <div className="flex items-center gap-2"> |
| 206 | <ButtonTooltip |
| 207 | type="default" |
| 208 | disabled={isRefreshing} |
| 209 | icon={<RefreshCw className={isRefreshing ? 'animate-spin' : ''} />} |
| 210 | className="w-7" |
| 211 | tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }} |
| 212 | onClick={onRefreshReport} |
| 213 | /> |
| 214 | <ReportSettings chartId={chartSyncId} /> |
| 215 | <LogsDatePicker |
| 216 | onSubmit={handleDatePickerChange} |
| 217 | value={datePickerValue} |
| 218 | helpers={datePickerHelpers} |
| 219 | /> |
| 220 | <UpgradePrompt |
| 221 | show={showUpgradePrompt} |
| 222 | setShowUpgradePrompt={setShowUpgradePrompt} |
| 223 | title="Report date range" |
| 224 | description="Report data can be stored for a maximum of 3 months depending on the plan that your project is on." |
| 225 | source="authReportDateRange" |
| 226 | /> |
| 227 | {selectedDateRange && ( |
| 228 | <div className="flex items-center gap-x-2 text-xs"> |
| 229 | <p className="text-foreground-light"> |
| 230 | {dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')} |
| 231 | </p> |
| 232 | <p className="text-foreground-light"> |
| 233 | <ArrowRight size={12} /> |
| 234 | </p> |
| 235 | <p className="text-foreground-light"> |
| 236 | {dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')} |
| 237 | </p> |
| 238 | </div> |
| 239 | )} |
| 240 | </div> |
| 241 | </div> |
| 242 | } |
| 243 | > |
| 244 | <div className="mt-8 flex flex-col gap-8 pb-8"> |
| 245 | <div className="flex flex-col gap-4" id="usage"> |
| 246 | <div> |
| 247 | <ReportSectionHeader |
| 248 | id="usage" |
| 249 | title="Usage" |
| 250 | description="Monitor user activity, sign-ins, sign-ups, and password reset requests to understand how users interact with your authentication system." |
| 251 | /> |
| 252 | <ReportsSelectFilter |
| 253 | label="Provider" |
| 254 | options={providerOptions} |
| 255 | value={usageProviderFilter || []} |
| 256 | onChange={setUsageProviderFilter} |
| 257 | isLoading={isRefreshing} |
| 258 | showSearch={true} |
| 259 | /> |
| 260 | </div> |
| 261 | <div className="grid md:grid-cols-2 gap-4"> |
| 262 | {usageReportConfig.map((metric) => ( |
| 263 | <ReportChartV2 |
| 264 | key={`${metric.id}`} |
| 265 | report={metric} |
| 266 | projectRef={ref!} |
| 267 | interval={selectedDateRange.interval} |
| 268 | startDate={selectedDateRange?.period_start?.date} |
| 269 | endDate={selectedDateRange?.period_end?.date} |
| 270 | updateDateRange={updateDateRange} |
| 271 | syncId={chartSyncId} |
| 272 | filters={{ provider: usageProviderFilter }} |
| 273 | highlightActions={highlightActions} |
| 274 | /> |
| 275 | ))} |
| 276 | </div> |
| 277 | </div> |
| 278 | |
| 279 | <div className="flex flex-col gap-4" id="monitoring"> |
| 280 | <div> |
| 281 | <ReportSectionHeader |
| 282 | id="monitoring" |
| 283 | title="Monitoring" |
| 284 | description="Track authentication errors by status code and error type to identify issues and improve user experience." |
| 285 | /> |
| 286 | |
| 287 | <ReportsNumericFilter |
| 288 | label="Status Code" |
| 289 | value={monitoringStatusCodeFilter} |
| 290 | onChange={setMonitoringStatusCodeFilter} |
| 291 | defaultOperator="=" |
| 292 | isLoading={isRefreshing} |
| 293 | /> |
| 294 | </div> |
| 295 | <div className="grid md:grid-cols-2 gap-4"> |
| 296 | {errorsReportConfig.map((metric) => ( |
| 297 | <ReportChartV2 |
| 298 | key={`${metric.id}`} |
| 299 | report={metric} |
| 300 | projectRef={ref!} |
| 301 | interval={selectedDateRange.interval} |
| 302 | startDate={selectedDateRange?.period_start?.date} |
| 303 | endDate={selectedDateRange?.period_end?.date} |
| 304 | updateDateRange={updateDateRange} |
| 305 | syncId={chartSyncId} |
| 306 | filters={{ status_code: monitoringStatusCodeFilter }} |
| 307 | highlightActions={highlightActions} |
| 308 | /> |
| 309 | ))} |
| 310 | </div> |
| 311 | </div> |
| 312 | |
| 313 | <div className="flex flex-col gap-4" id="performance"> |
| 314 | <ReportSectionHeader |
| 315 | id="performance" |
| 316 | title="Performance" |
| 317 | description="Monitor sign-in and sign-up performance metrics including average, percentiles, and request counts to ensure optimal authentication speed." |
| 318 | /> |
| 319 | <div className="grid md:grid-cols-2 gap-4"> |
| 320 | {latencyReportConfig.map((metric) => ( |
| 321 | <ReportChartV2 |
| 322 | key={`${metric.id}`} |
| 323 | report={metric} |
| 324 | projectRef={ref!} |
| 325 | interval={selectedDateRange.interval} |
| 326 | startDate={selectedDateRange?.period_start?.date} |
| 327 | endDate={selectedDateRange?.period_end?.date} |
| 328 | updateDateRange={updateDateRange} |
| 329 | syncId={chartSyncId} |
| 330 | filters={{}} |
| 331 | highlightActions={highlightActions} |
| 332 | /> |
| 333 | ))} |
| 334 | </div> |
| 335 | </div> |
| 336 | <div> |
| 337 | <div className="mb-4 space-y-4"> |
| 338 | <ReportSectionHeader |
| 339 | id="auth-api-gateway" |
| 340 | title="Auth API Gateway" |
| 341 | description="Monitor user activity, sign-ins, sign-ups, and password reset requests to understand how users interact with your authentication system." |
| 342 | /> |
| 343 | <ReportFilterBar |
| 344 | filters={filters} |
| 345 | onAddFilter={addFilter} |
| 346 | onRemoveFilters={removeFilters} |
| 347 | isLoading={isLoadingData || isRefetching} |
| 348 | hideDatepicker={true} |
| 349 | datepickerHelpers={datePickerHelpers} |
| 350 | selectedProduct={'auth'} |
| 351 | showDatabaseSelector={false} |
| 352 | /> |
| 353 | </div> |
| 354 | <SharedAPIReport |
| 355 | data={data} |
| 356 | error={error} |
| 357 | isLoading={isLoading} |
| 358 | isRefetching={isRefetching} |
| 359 | sql={sql} |
| 360 | /> |
| 361 | </div> |
| 362 | </div> |
| 363 | </ReportStickyNav> |
| 364 | <div className="pb-8"> |
| 365 | <ObservabilityLink /> |
| 366 | </div> |
| 367 | </> |
| 368 | ) |
| 369 | } |