Skip to content

Commit f05ed08

Browse files
feat: Implement proxy health check (#716)
1 parent d81f19d commit f05ed08

File tree

14 files changed

+295
-116
lines changed

14 files changed

+295
-116
lines changed

package-lock.json

Lines changed: 75 additions & 99 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
"electron-squirrel-startup": "^1.0.1",
116116
"find-process": "^1.4.7",
117117
"fuse.js": "^7.0.0",
118+
"https-proxy-agent": "^7.0.6",
118119
"immer": "^10.1.1",
119120
"jsonrepair": "^3.8.0",
120121
"keyboardjs": "^2.7.0",

resources/json_output.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,21 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
159159

160160

161161
def request(flow: http.HTTPFlow) -> None:
162+
# we don't want to log health check requests to stdout and be captured by the client
163+
if flow.request.headers.get("X-K6-Studio-Health-Check", "").lower() == "true":
164+
flow.request.anticache()
165+
return
162166

163167
data = flow_to_json(flow)
164168
data = json.dumps(data)
165169
print(data, flush=True)
166170

167171

168172
def response(flow: http.HTTPFlow) -> None:
173+
# we don't want to log health check requests to stdout and be captured by the client
174+
if flow.request.headers.get("X-K6-Studio-Health-Check", "").lower() == "true":
175+
flow.request.anticache()
176+
return
169177

170178
data = flow_to_json(flow)
171179
data = json.dumps(data)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Badge, Callout, Tooltip } from '@radix-ui/themes'
2+
import { TriangleAlert } from 'lucide-react'
3+
4+
import { useStudioUIStore } from '@/store/ui'
5+
6+
import { ExternalLink } from './ExternalLink'
7+
import { TextButton } from './TextButton'
8+
9+
export function ProxyHealthWarning() {
10+
const openSettingsDialog = useStudioUIStore(
11+
(state) => state.openSettingsDialog
12+
)
13+
14+
return (
15+
<Callout.Root>
16+
<Callout.Icon>
17+
<TriangleAlert />
18+
</Callout.Icon>
19+
<Callout.Text>
20+
<strong>Proxy health check failed</strong>
21+
<br />
22+
Grafana k6 Studio cannot establish connection to the Internet. Unless
23+
this is expected due to your internal network configuration, check{' '}
24+
<TextButton onClick={() => openSettingsDialog('proxy')}>
25+
proxy settings
26+
</TextButton>{' '}
27+
or learn more in the{' '}
28+
<ExternalLink href="https://grafana.com/docs/k6-studio/troubleshoot/#502-bad-gateway-error">
29+
troubleshooting guide
30+
</ExternalLink>{' '}
31+
.
32+
</Callout.Text>
33+
</Callout.Root>
34+
)
35+
}
36+
37+
export function ProxyHealthBadge() {
38+
return (
39+
<Tooltip content="Grafana k6 Studio cannot establish connection to the Internet. Unless this is expected due to your internal network configuration, check your proxy settings.">
40+
<Badge color="orange" ml="2">
41+
<TriangleAlert />
42+
Proxy health check failed
43+
</Badge>
44+
</Tooltip>
45+
)
46+
}

src/handlers/proxy/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ipcMain } from 'electron'
22

3+
import { checkProxyHealth } from '@/main/healthCheck'
34
import { launchProxyAndAttachEmitter, stopProxyProcess } from '@/main/proxy'
45
import { browserWindowFromEvent } from '@/utils/electron'
56

@@ -24,4 +25,10 @@ export function initialize() {
2425
console.info(`${ProxyHandler.GetStatus} event received`)
2526
return k6StudioState.proxyStatus
2627
})
28+
29+
ipcMain.handle(ProxyHandler.CheckHealth, async () => {
30+
console.info(`${ProxyHandler.CheckHealth} event received`)
31+
const isHealthy = await checkProxyHealth()
32+
return isHealthy
33+
})
2734
}

src/handlers/proxy/preload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ export function getProxyStatus() {
2525
export function onProxyStatusChange(callback: (status: ProxyStatus) => void) {
2626
return createListener(ProxyHandler.ChangeStatus, callback)
2727
}
28+
29+
export function checkProxyHealth() {
30+
return ipcRenderer.invoke(ProxyHandler.CheckHealth) as Promise<boolean>
31+
}

src/handlers/proxy/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export enum ProxyHandler {
55
GetStatus = 'proxy:status:get',
66
ChangeStatus = 'proxy:status:change',
77
Data = 'proxy:data',
8+
CheckHealth = 'proxy:health:check',
89
}

src/hooks/useProxyHealthCheck.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useQuery } from '@tanstack/react-query'
2+
import { useEffect } from 'react'
3+
4+
import { ProxyStatus } from '@/types'
5+
6+
export function useProxyHealthCheck(proxyStatus?: ProxyStatus) {
7+
const { data: isProxyHealthy, refetch } = useQuery({
8+
queryKey: ['proxy-health'],
9+
queryFn: () => window.studio.proxy.checkProxyHealth(),
10+
networkMode: 'always',
11+
placeholderData: true,
12+
// only refetch when the proxy is unhealthy
13+
refetchInterval: (isProxyHealthy) =>
14+
isProxyHealthy.state.data ? false : 2000,
15+
})
16+
17+
useEffect(() => {
18+
// This auto-refetch is needed to handle cases where you change your settings (from unhealthy to healthy) with the Recorder page opened.
19+
// This ensures that a new health check is performed as soon as the proxy restarts (without need to reopen the page again).
20+
const fetchProxyHealth = async () => {
21+
if (proxyStatus === 'online') {
22+
await refetch()
23+
}
24+
}
25+
void fetchProxyHealth()
26+
}, [proxyStatus, refetch])
27+
28+
return { isProxyHealthy, refetchProxyHealth: refetch }
29+
}

src/main/file.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os from 'os'
12
import path from 'path'
23

34
import {
@@ -71,3 +72,11 @@ export function getFilePath(
7172
return exhaustive(file.type)
7273
}
7374
}
75+
76+
export function expandHomeDir(inputPath?: string) {
77+
if (!inputPath) return inputPath
78+
if (inputPath.startsWith('~')) {
79+
return path.join(os.homedir(), inputPath.slice(1))
80+
}
81+
return inputPath
82+
}

0 commit comments

Comments
 (0)