From cf35c0dfc53e0d51f134c19834bd6e142fad02db Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 2 Aug 2023 14:49:24 -0300 Subject: [PATCH] feat(site): add health warning and a health monitor page (#8844) --- coderd/apidoc/docs.go | 6 +- coderd/apidoc/swagger.json | 6 +- codersdk/deployment.go | 4 + docs/api/schemas.md | 1 + site/src/AppRouter.tsx | 3 + site/src/api/api.ts | 12 + site/src/api/typesGenerated.ts | 2 + .../components/Dashboard/DashboardLayout.tsx | 2 + .../src/components/Dashboard/HealthBanner.tsx | 45 ++ .../DeploySettingsLayout/Sidebar.tsx | 9 + .../PageHeader/FullWidthPageHeader.tsx | 17 +- site/src/components/Stats/Stats.tsx | 11 +- .../SyntaxHighlighter/SyntaxHighlighter.tsx | 8 +- .../pages/HealthPage/HealthPage.stories.tsx | 33 ++ site/src/pages/HealthPage/HealthPage.tsx | 247 +++++++++++ site/src/testHelpers/entities.ts | 419 ++++++++++++++++++ site/src/testHelpers/handlers.ts | 4 + 17 files changed, 814 insertions(+), 15 deletions(-) create mode 100644 site/src/components/Dashboard/HealthBanner.tsx create mode 100644 site/src/pages/HealthPage/HealthPage.stories.tsx create mode 100644 site/src/pages/HealthPage/HealthPage.tsx diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 4d94e9bc21..6ce7eb21c0 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8024,7 +8024,8 @@ const docTemplate = `{ "tailnet_pg_coordinator", "single_tailnet", "template_restart_requirement", - "template_insights_page" + "template_insights_page", + "deployment_health_page" ], "x-enum-varnames": [ "ExperimentMoons", @@ -8032,7 +8033,8 @@ const docTemplate = `{ "ExperimentTailnetPGCoordinator", "ExperimentSingleTailnet", "ExperimentTemplateRestartRequirement", - "ExperimentTemplateInsightsPage" + "ExperimentTemplateInsightsPage", + "ExperimentDeploymentHealthPage" ] }, "codersdk.Feature": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d77432f76a..500307a23f 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7185,7 +7185,8 @@ "tailnet_pg_coordinator", "single_tailnet", "template_restart_requirement", - "template_insights_page" + "template_insights_page", + "deployment_health_page" ], "x-enum-varnames": [ "ExperimentMoons", @@ -7193,7 +7194,8 @@ "ExperimentTailnetPGCoordinator", "ExperimentSingleTailnet", "ExperimentTemplateRestartRequirement", - "ExperimentTemplateInsightsPage" + "ExperimentTemplateInsightsPage", + "ExperimentDeploymentHealthPage" ] }, "codersdk.Feature": { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index e27731b6aa..305e14f944 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1871,6 +1871,9 @@ const ( // Insights page ExperimentTemplateInsightsPage Experiment = "template_insights_page" + // Deployment health page + ExperimentDeploymentHealthPage Experiment = "deployment_health_page" + // Add new experiments here! // ExperimentExample Experiment = "example" ) @@ -1881,6 +1884,7 @@ const ( // not be included here and will be essentially hidden. var ExperimentsAll = Experiments{ ExperimentTemplateInsightsPage, + ExperimentDeploymentHealthPage, } // Experiments is a list of experiments that are enabled for the deployment. diff --git a/docs/api/schemas.md b/docs/api/schemas.md index b62322a54b..db7ec28732 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2677,6 +2677,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `single_tailnet` | | `template_restart_requirement` | | `template_insights_page` | +| `deployment_health_page` | ## codersdk.Feature diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 34cbc0ed6d..449d4925fe 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -183,6 +183,7 @@ const TemplateInsightsPage = lazy( () => import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"), ) +const HealthPage = lazy(() => import("./pages/HealthPage/HealthPage")) export const AppRouter: FC = () => { return ( @@ -197,6 +198,8 @@ export const AppRouter: FC = () => { }> } /> + } /> + } /> } /> diff --git a/site/src/api/api.ts b/site/src/api/api.ts index a1ac0a1636..bd2f36967f 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1405,3 +1405,15 @@ export const getInsightsTemplate = async ( const response = await axios.get(`/api/v2/insights/templates?${params}`) return response.data } + +export const getHealth = () => { + return axios.get<{ + healthy: boolean + time: string + coder_version: string + derp: { healthy: boolean } + access_url: { healthy: boolean } + websocket: { healthy: boolean } + database: { healthy: boolean } + }>("/api/v2/debug/health") +} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 07da6e5de4..d483fa3ccc 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1563,6 +1563,7 @@ export const Entitlements: Entitlement[] = [ // From codersdk/deployment.go export type Experiment = + | "deployment_health_page" | "moons" | "single_tailnet" | "tailnet_pg_coordinator" @@ -1570,6 +1571,7 @@ export type Experiment = | "template_restart_requirement" | "workspace_actions" export const Experiments: Experiment[] = [ + "deployment_health_page", "moons", "single_tailnet", "tailnet_pg_coordinator", diff --git a/site/src/components/Dashboard/DashboardLayout.tsx b/site/src/components/Dashboard/DashboardLayout.tsx index 7ffd518d5f..e7f01c9e3a 100644 --- a/site/src/components/Dashboard/DashboardLayout.tsx +++ b/site/src/components/Dashboard/DashboardLayout.tsx @@ -16,6 +16,7 @@ import Box from "@mui/material/Box" import InfoOutlined from "@mui/icons-material/InfoOutlined" import Button from "@mui/material/Button" import { docs } from "utils/docs" +import { HealthBanner } from "./HealthBanner" export const DashboardLayout: FC = () => { const styles = useStyles() @@ -30,6 +31,7 @@ export const DashboardLayout: FC = () => { return ( <> + {canViewDeployment && } diff --git a/site/src/components/Dashboard/HealthBanner.tsx b/site/src/components/Dashboard/HealthBanner.tsx new file mode 100644 index 0000000000..a8f5a2d065 --- /dev/null +++ b/site/src/components/Dashboard/HealthBanner.tsx @@ -0,0 +1,45 @@ +import { Alert } from "components/Alert/Alert" +import { Link as RouterLink } from "react-router-dom" +import Link from "@mui/material/Link" +import { colors } from "theme/colors" +import { useQuery } from "@tanstack/react-query" +import { getHealth } from "api/api" +import { useDashboard } from "./DashboardProvider" + +export const HealthBanner = () => { + const { data: healthStatus } = useQuery({ + queryKey: ["health"], + queryFn: () => getHealth(), + }) + const dashboard = useDashboard() + const hasHealthIssues = healthStatus && !healthStatus.data.healthy + + if ( + dashboard.experiments.includes("deployment_health_page") && + hasHealthIssues + ) { + return ( + + We have detected problems with your Coder deployment. Please{" "} + + inspect the health status + + . + + ) + } + + return null +} diff --git a/site/src/components/DeploySettingsLayout/Sidebar.tsx b/site/src/components/DeploySettingsLayout/Sidebar.tsx index 1f365d8cea..e8b0445e4c 100644 --- a/site/src/components/DeploySettingsLayout/Sidebar.tsx +++ b/site/src/components/DeploySettingsLayout/Sidebar.tsx @@ -6,6 +6,7 @@ import LockRounded from "@mui/icons-material/LockOutlined" import Globe from "@mui/icons-material/PublicOutlined" import HubOutlinedIcon from "@mui/icons-material/HubOutlined" import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined" +import MonitorHeartOutlined from "@mui/icons-material/MonitorHeartOutlined" import { GitIcon } from "components/Icons/GitIcon" import { Stack } from "components/Stack/Stack" import { ElementType, PropsWithChildren, ReactNode, FC } from "react" @@ -93,6 +94,14 @@ export const Sidebar: React.FC = () => { > Security + {dashboard.experiments.includes("deployment_health_page") && ( + } + > + Health + + )} ) } diff --git a/site/src/components/PageHeader/FullWidthPageHeader.tsx b/site/src/components/PageHeader/FullWidthPageHeader.tsx index 7e381848ec..ced199ee8d 100644 --- a/site/src/components/PageHeader/FullWidthPageHeader.tsx +++ b/site/src/components/PageHeader/FullWidthPageHeader.tsx @@ -1,11 +1,17 @@ import { makeStyles } from "@mui/styles" import { FC, PropsWithChildren } from "react" +import { combineClasses } from "utils/combineClasses" -export const FullWidthPageHeader: FC = ({ children }) => { +export const FullWidthPageHeader: FC< + PropsWithChildren & { sticky?: boolean } +> = ({ children, sticky = true }) => { const styles = useStyles() return ( -
+
{children}
) @@ -35,8 +41,7 @@ const useStyles = makeStyles((theme) => ({ display: "flex", alignItems: "center", gap: theme.spacing(6), - position: "sticky", - top: 0, + zIndex: 10, flexWrap: "wrap", @@ -48,6 +53,10 @@ const useStyles = makeStyles((theme) => ({ flexDirection: "column", }, }, + sticky: { + position: "sticky", + top: 0, + }, actions: { marginLeft: "auto", [theme.breakpoints.down("md")]: { diff --git a/site/src/components/Stats/Stats.tsx b/site/src/components/Stats/Stats.tsx index 5adb4174f5..c2674e9c7f 100644 --- a/site/src/components/Stats/Stats.tsx +++ b/site/src/components/Stats/Stats.tsx @@ -1,11 +1,12 @@ +import Box from "@mui/material/Box" import { makeStyles } from "@mui/styles" import { ComponentProps, FC, PropsWithChildren } from "react" import { combineClasses } from "utils/combineClasses" -export const Stats: FC> = (props) => { +export const Stats: FC> = (props) => { const styles = useStyles() return ( -
@@ -16,18 +17,18 @@ export const StatsItem: FC< { label: string value: string | number | JSX.Element - } & ComponentProps<"div"> + } & ComponentProps > = ({ label, value, ...divProps }) => { const styles = useStyles() return ( -
{label}: {value} -
+ ) } diff --git a/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx b/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx index d37c884f1b..2dfe42d17c 100644 --- a/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx +++ b/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx @@ -1,4 +1,4 @@ -import { FC } from "react" +import { ComponentProps, FC } from "react" import Editor, { DiffEditor, loader } from "@monaco-editor/react" import * as monaco from "monaco-editor" import { useCoderTheme } from "./coderTheme" @@ -9,8 +9,10 @@ loader.config({ monaco }) export const SyntaxHighlighter: FC<{ value: string language: string + editorProps?: ComponentProps & + ComponentProps compareWith?: string -}> = ({ value, compareWith, language }) => { +}> = ({ value, compareWith, language, editorProps }) => { const styles = useStyles() const hasDiff = compareWith && value !== compareWith const coderTheme = useCoderTheme() @@ -25,6 +27,7 @@ export const SyntaxHighlighter: FC<{ renderSideBySide: true, readOnly: true, }, + ...editorProps, } if (coderTheme.isLoading) { @@ -46,5 +49,6 @@ const useStyles = makeStyles((theme) => ({ wrapper: { padding: theme.spacing(1, 0), background: theme.palette.background.paper, + height: "100%", }, })) diff --git a/site/src/pages/HealthPage/HealthPage.stories.tsx b/site/src/pages/HealthPage/HealthPage.stories.tsx new file mode 100644 index 0000000000..dd7baddb70 --- /dev/null +++ b/site/src/pages/HealthPage/HealthPage.stories.tsx @@ -0,0 +1,33 @@ +import { Meta, StoryObj } from "@storybook/react" +import { HealthPageView } from "./HealthPage" +import { MockHealth } from "testHelpers/entities" + +const meta: Meta = { + title: "pages/HealthPageView", + component: HealthPageView, + args: { + tab: { + value: "derp", + set: () => {}, + }, + healthStatus: MockHealth, + }, +} + +export default meta +type Story = StoryObj + +export const HealthPage: Story = {} + +export const UnhealthPage: Story = { + args: { + healthStatus: { + ...MockHealth, + healthy: false, + derp: { + ...MockHealth.derp, + healthy: false, + }, + }, + }, +} diff --git a/site/src/pages/HealthPage/HealthPage.tsx b/site/src/pages/HealthPage/HealthPage.tsx new file mode 100644 index 0000000000..55f5f820f8 --- /dev/null +++ b/site/src/pages/HealthPage/HealthPage.tsx @@ -0,0 +1,247 @@ +import Box from "@mui/material/Box" +import { useQuery } from "@tanstack/react-query" +import { getHealth } from "api/api" +import { Loader } from "components/Loader/Loader" +import { useTab } from "hooks" +import { Helmet } from "react-helmet-async" +import { pageTitle } from "utils/page" +import { colors } from "theme/colors" +import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined" +import ErrorOutline from "@mui/icons-material/ErrorOutline" +import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter" +import { Stack } from "components/Stack/Stack" +import { + FullWidthPageHeader, + PageHeaderTitle, + PageHeaderSubtitle, +} from "components/PageHeader/FullWidthPageHeader" +import { Stats, StatsItem } from "components/Stats/Stats" +import { makeStyles } from "@mui/styles" +import { createDayString } from "utils/createDayString" + +const sections = { + derp: "DERP", + access_url: "Access URL", + websocket: "Websocket", + database: "Database", +} as const + +export default function HealthPage() { + const tab = useTab("tab", "derp") + const { data: healthStatus } = useQuery({ + queryKey: ["health"], + queryFn: () => getHealth(), + refetchInterval: 10_000, + }) + + return ( + <> + + {pageTitle("Health")} + + + {healthStatus ? ( + + ) : ( + + )} + + ) +} + +export function HealthPageView({ + healthStatus, + tab, +}: { + healthStatus: Awaited>["data"] + tab: ReturnType +}) { + const styles = useStyles() + + return ( + + + + {healthStatus.healthy ? ( + theme.palette.success.light, + }} + /> + ) : ( + theme.palette.error.main, + }} + /> + )} + +
+ + {healthStatus.healthy ? "Healthy" : "Unhealthy"} + + + {healthStatus.healthy + ? "All systems operational" + : "Some issues have been detected"} + +
+
+ + + + + +
+ + theme.spacing(32), + flexShrink: 0, + borderRight: (theme) => `1px solid ${theme.palette.divider}`, + height: "100%", + }} + > + theme.palette.text.secondary, + padding: (theme) => theme.spacing(1.5, 3), + letterSpacing: "0.5px", + }} + > + Health + + + {Object.keys(sections) + .sort() + .map((key) => { + const label = sections[key as keyof typeof sections] + const isActive = tab.value === key + const isHealthy = + healthStatus[key as keyof typeof sections].healthy + + return ( + { + tab.set(key) + }} + sx={{ + background: isActive ? colors.gray[13] : "none", + border: "none", + fontSize: 14, + width: "100%", + display: "flex", + alignItems: "center", + gap: 1, + textAlign: "left", + height: 36, + padding: (theme) => theme.spacing(0, 3), + cursor: "pointer", + pointerEvents: isActive ? "none" : "auto", + color: (theme) => + isActive + ? theme.palette.text.primary + : theme.palette.text.secondary, + "&:hover": { + background: (theme) => theme.palette.action.hover, + color: (theme) => theme.palette.text.primary, + }, + }} + > + {isHealthy ? ( + theme.palette.success.light, + }} + /> + ) : ( + theme.palette.error.main, + }} + /> + )} + {label} + + ) + })} + + + {/* 62px - navbar and 36px - the bottom bar */} + + + + +
+ ) +} + +const useStyles = makeStyles((theme) => ({ + stats: { + padding: 0, + border: 0, + gap: theme.spacing(6), + rowGap: theme.spacing(3), + flex: 1, + + [theme.breakpoints.down("md")]: { + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + gap: theme.spacing(1), + }, + }, + + statsItem: { + flexDirection: "column", + gap: 0, + padding: 0, + + "& > span:first-of-type": { + fontSize: 12, + fontWeight: 500, + }, + }, +})) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index da79830725..8b20854801 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1862,3 +1862,422 @@ export const MockLicenseResponse: GetLicensesResponse[] = [ }, }, ] + +export const MockHealth = { + time: "2023-08-01T16:51:03.29792825Z", + healthy: true, + failing_sections: null, + derp: { + healthy: true, + regions: { + "999": { + healthy: true, + region: { + EmbeddedRelay: true, + RegionID: 999, + RegionCode: "coder", + RegionName: "Council Bluffs, Iowa", + Nodes: [ + { + Name: "999stun0", + RegionID: 999, + HostName: "stun.l.google.com", + STUNPort: 19302, + STUNOnly: true, + }, + { + Name: "999b", + RegionID: 999, + HostName: "dev.coder.com", + STUNPort: -1, + DERPPort: 443, + }, + ], + }, + node_reports: [ + { + healthy: true, + node: { + Name: "999stun0", + RegionID: 999, + HostName: "stun.l.google.com", + STUNPort: 19302, + STUNOnly: true, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: false, + round_trip_ping: 0, + uses_websocket: false, + client_logs: [], + client_errs: [], + error: null, + stun: { + Enabled: true, + CanSTUN: true, + Error: null, + }, + }, + { + healthy: true, + node: { + Name: "999b", + RegionID: 999, + HostName: "dev.coder.com", + STUNPort: -1, + DERPPort: 443, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: true, + round_trip_ping: 7674330, + uses_websocket: false, + client_logs: [ + [ + "derphttp.Client.Connect: connecting to https://dev.coder.com/derp", + ], + [ + "derphttp.Client.Connect: connecting to https://dev.coder.com/derp", + ], + ], + client_errs: [[], []], + error: null, + stun: { + Enabled: false, + CanSTUN: false, + Error: null, + }, + }, + ], + error: null, + }, + "10007": { + healthy: true, + region: { + EmbeddedRelay: false, + RegionID: 10007, + RegionCode: "coder_sydney", + RegionName: "sydney", + Nodes: [ + { + Name: "10007stun0", + RegionID: 10007, + HostName: "stun.l.google.com", + STUNPort: 19302, + STUNOnly: true, + }, + { + Name: "10007a", + RegionID: 10007, + HostName: "sydney.dev.coder.com", + STUNPort: -1, + DERPPort: 443, + }, + ], + }, + node_reports: [ + { + healthy: true, + node: { + Name: "10007stun0", + RegionID: 10007, + HostName: "stun.l.google.com", + STUNPort: 19302, + STUNOnly: true, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: false, + round_trip_ping: 0, + uses_websocket: false, + client_logs: [], + client_errs: [], + error: null, + stun: { + Enabled: true, + CanSTUN: true, + Error: null, + }, + }, + { + healthy: true, + node: { + Name: "10007a", + RegionID: 10007, + HostName: "sydney.dev.coder.com", + STUNPort: -1, + DERPPort: 443, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: true, + round_trip_ping: 170527034, + uses_websocket: false, + client_logs: [ + [ + "derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp", + ], + [ + "derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp", + ], + ], + client_errs: [[], []], + error: null, + stun: { + Enabled: false, + CanSTUN: false, + Error: null, + }, + }, + ], + error: null, + }, + "10008": { + healthy: true, + region: { + EmbeddedRelay: false, + RegionID: 10008, + RegionCode: "coder_europe-frankfurt", + RegionName: "europe-frankfurt", + Nodes: [ + { + Name: "10008stun0", + RegionID: 10008, + HostName: "stun.l.google.com", + STUNPort: 19302, + STUNOnly: true, + }, + { + Name: "10008a", + RegionID: 10008, + HostName: "europe.dev.coder.com", + STUNPort: -1, + DERPPort: 443, + }, + ], + }, + node_reports: [ + { + healthy: true, + node: { + Name: "10008stun0", + RegionID: 10008, + HostName: "stun.l.google.com", + STUNPort: 19302, + STUNOnly: true, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: false, + round_trip_ping: 0, + uses_websocket: false, + client_logs: [], + client_errs: [], + error: null, + stun: { + Enabled: true, + CanSTUN: true, + Error: null, + }, + }, + { + healthy: true, + node: { + Name: "10008a", + RegionID: 10008, + HostName: "europe.dev.coder.com", + STUNPort: -1, + DERPPort: 443, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: true, + round_trip_ping: 111329690, + uses_websocket: false, + client_logs: [ + [ + "derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp", + ], + [ + "derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp", + ], + ], + client_errs: [[], []], + error: null, + stun: { + Enabled: false, + CanSTUN: false, + Error: null, + }, + }, + ], + error: null, + }, + "10009": { + healthy: true, + region: { + EmbeddedRelay: false, + RegionID: 10009, + RegionCode: "coder_brazil-saopaulo", + RegionName: "brazil-saopaulo", + Nodes: [ + { + Name: "10009stun0", + RegionID: 10009, + HostName: "stun.l.google.com", + STUNPort: 19302, + STUNOnly: true, + }, + { + Name: "10009a", + RegionID: 10009, + HostName: "brazil.dev.coder.com", + STUNPort: -1, + DERPPort: 443, + }, + ], + }, + node_reports: [ + { + healthy: true, + node: { + Name: "10009stun0", + RegionID: 10009, + HostName: "stun.l.google.com", + STUNPort: 19302, + STUNOnly: true, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: false, + round_trip_ping: 0, + uses_websocket: false, + client_logs: [], + client_errs: [], + error: null, + stun: { + Enabled: true, + CanSTUN: true, + Error: null, + }, + }, + { + healthy: true, + node: { + Name: "10009a", + RegionID: 10009, + HostName: "brazil.dev.coder.com", + STUNPort: -1, + DERPPort: 443, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: true, + round_trip_ping: 138185506, + uses_websocket: false, + client_logs: [ + [ + "derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp", + ], + [ + "derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp", + ], + ], + client_errs: [[], []], + error: null, + stun: { + Enabled: false, + CanSTUN: false, + Error: null, + }, + }, + ], + error: null, + }, + }, + netcheck: { + UDP: true, + IPv6: false, + IPv4: true, + IPv6CanSend: false, + IPv4CanSend: true, + OSHasIPv6: true, + ICMPv4: false, + MappingVariesByDestIP: false, + HairPinning: null, + UPnP: false, + PMP: false, + PCP: false, + PreferredDERP: 999, + RegionLatency: { + "999": 1638180, + "10007": 174853022, + "10008": 112142029, + "10009": 138855606, + }, + RegionV4Latency: { + "999": 1638180, + "10007": 174853022, + "10008": 112142029, + "10009": 138855606, + }, + RegionV6Latency: {}, + GlobalV4: "34.71.26.24:55368", + GlobalV6: "", + CaptivePortal: null, + }, + netcheck_err: null, + netcheck_logs: [ + "netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (9b07930007da49dd7df79bc7) in 1.791799ms", + "netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (7397fec097f1d5b01364566b) in 1.791529ms", + "netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (1fdaaa016ca386485f097f68) in 2.192899ms", + "netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (2596fe60895fbd9542823a76) in 2.146459ms", + "netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (19ec320f3b76e8b027b06d3e) in 2.139619ms", + "netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (a17973bc57c35e606c0f46f5) in 2.131089ms", + "netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (c958e15209d139a6e410f13a) in 2.127549ms", + "netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (284a1b64dff22f40a3514524) in 2.107549ms", + "netcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted", + "netcheck: [v1] report: udp=true v6=false v6os=true mapvarydest=false hair= portmap= v4a=34.71.26.24:55368 derp=999 derpdist=999v4:2ms,10007v4:175ms,10008v4:112ms,10009v4:139ms", + ], + error: null, + }, + access_url: { + access_url: "https://dev.coder.com", + healthy: true, + reachable: true, + status_code: 200, + healthz_response: "OK", + error: null, + }, + websocket: { + healthy: true, + response: { + body: "", + code: 101, + }, + error: null, + }, + database: { + healthy: true, + reachable: true, + latency: 92570, + error: null, + }, + coder_version: "v0.27.1-devel+c575292", +} diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 27418b2c4a..9d90e09724 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -387,4 +387,8 @@ export const handlers = [ rest.get("/api/v2/workspaceagents/:agent/logs", (_, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockWorkspaceAgentLogs)) }), + + rest.get("/api/v2/debug/health", (_, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockHealth)) + }), ]