Skip to content

Commit c706842

Browse files
committed
Report and track WebSocket errors
1 parent cf3d2cf commit c706842

File tree

5 files changed

+68
-15
lines changed

5 files changed

+68
-15
lines changed

ui/frontend/actions.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ export enum ActionType {
138138
WSExecuteResponse = 'WS_EXECUTE_RESPONSE',
139139
}
140140

141+
export const WebSocketError = z.object({
142+
type: z.literal(ActionType.WebSocketError),
143+
error: z.string(),
144+
});
145+
export type WebSocketError = z.infer<typeof WebSocketError>;
146+
141147
const ExecuteExtra = z.object({
142148
isAutoBuild: z.boolean(),
143149
});
@@ -851,7 +857,7 @@ export const browserWidthChanged = (isSmall: boolean) =>
851857
export const splitRatioChanged = () =>
852858
createAction(ActionType.SplitRatioChanged);
853859

854-
export const websocketError = () => createAction(ActionType.WebSocketError);
860+
export const websocketError = (error: string): WebSocketError => createAction(ActionType.WebSocketError, { error });
855861
export const websocketConnected = () => createAction(ActionType.WebSocketConnected);
856862
export const websocketDisconnected = () => createAction(ActionType.WebSocketDisconnected);
857863
export const websocketFeatureFlagEnabled = () => createAction(ActionType.WebSocketFeatureFlagEnabled);

ui/frontend/reducers/websocket.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Action, ActionType } from '../actions';
22

33
export type State = {
44
connected: boolean;
5+
error?: string;
56
featureFlagEnabled: boolean;
67
};
78

@@ -13,13 +14,13 @@ const DEFAULT: State = {
1314
export default function websocket(state = DEFAULT, action: Action): State {
1415
switch (action.type) {
1516
case ActionType.WebSocketConnected:
16-
return { ...state, connected: true };
17+
return { ...state, connected: true, error: undefined };
1718

1819
case ActionType.WebSocketDisconnected:
1920
return { ...state, connected: false };
2021

2122
case ActionType.WebSocketError:
22-
return { ...state };
23+
return { ...state, error: action.error };
2324

2425
case ActionType.WebSocketFeatureFlagEnabled:
2526
return { ...state, featureFlagEnabled: true };

ui/frontend/websocketMiddleware.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
import { Middleware } from 'redux';
22
import { z } from 'zod';
33

4-
import { ActionType, websocketError, websocketConnected, websocketDisconnected, WSExecuteResponse } from './actions';
4+
import {
5+
ActionType,
6+
WSExecuteResponse,
7+
WebSocketError,
8+
websocketConnected,
9+
websocketDisconnected,
10+
websocketError,
11+
} from './actions';
512

6-
const WSMessageResponse = z.discriminatedUnion('type', [WSExecuteResponse]);
13+
const WSMessageResponse = z.discriminatedUnion('type', [WebSocketError, WSExecuteResponse]);
714

8-
const reportWebSocketError = async () => {
15+
const reportWebSocketError = async (error: string) => {
916
try {
1017
await fetch('/nowebsocket', {
1118
method: 'post',
1219
headers: {
13-
'Content-Length': '0',
20+
'Content-Type': 'application/json',
1421
},
22+
body: JSON.stringify({ error }),
1523
});
1624
} catch (reportError) {
17-
console.log('Unable to report WebSocket error', reportError);
25+
console.log('Unable to report WebSocket error', error, reportError);
1826
}
1927
}
2028

@@ -26,7 +34,8 @@ const openWebSocket = (currentLocation: Location) => {
2634
} catch (e) {
2735
// WebSocket URL error or WebSocket is not supported by browser.
2836
// Assume it's the second case since URL error is easy to notice.
29-
reportWebSocketError()
37+
const detail = (e instanceof Error) ? e.toString() : 'An unknown error occurred';
38+
reportWebSocketError(`Could not create the WebSocket: ${detail}`)
3039

3140
return null;
3241
}
@@ -45,8 +54,11 @@ export const websocketMiddleware = (window: Window): Middleware => store => {
4554
});
4655

4756
socket.addEventListener('error', () => {
48-
store.dispatch(websocketError());
49-
reportWebSocketError();
57+
// We cannot get detailed information about the failure
58+
// https://stackoverflow.com/a/31003057/155423
59+
const error = 'Generic WebSocket Error';
60+
store.dispatch(websocketError(error));
61+
reportWebSocketError(error);
5062
});
5163

5264
// TODO: reconnect on error? (if ever connected? if < n failures?)

ui/src/server_axum.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub(crate) async fn serve(config: Config) {
8585
.route("/metrics", get(metrics))
8686
.route("/websocket", get(websocket))
8787
.route("/nowebsocket", post(nowebsocket))
88+
.route("/whynowebsocket", get(whynowebsocket))
8889
.layer(Extension(Arc::new(SandboxCache::default())))
8990
.layer(Extension(config.github_token()));
9091

@@ -397,10 +398,34 @@ async fn websocket(ws: WebSocketUpgrade) -> impl IntoResponse {
397398
ws.on_upgrade(websocket::handle)
398399
}
399400

400-
async fn nowebsocket() {
401+
#[derive(Debug, serde::Deserialize)]
402+
#[serde(rename_all = "camelCase")]
403+
struct NoWebSocketRequest {
404+
#[serde(default)]
405+
error: String,
406+
}
407+
408+
async fn nowebsocket(Json(req): Json<NoWebSocketRequest>) {
409+
record_websocket_error(req.error);
401410
UNAVAILABLE_WS.inc();
402411
}
403412

413+
lazy_static::lazy_static! {
414+
static ref WS_ERRORS: std::sync::Mutex<std::collections::HashMap<String, usize>> = Default::default();
415+
}
416+
417+
fn record_websocket_error(error: String) {
418+
*WS_ERRORS
419+
.lock()
420+
.unwrap_or_else(|e| e.into_inner())
421+
.entry(error)
422+
.or_default() += 1;
423+
}
424+
425+
async fn whynowebsocket() -> String {
426+
format!("{:#?}", WS_ERRORS.lock().unwrap_or_else(|e| e.into_inner()))
427+
}
428+
404429
#[derive(Debug)]
405430
struct MetricsAuthorization;
406431

ui/src/server_axum/websocket.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,18 @@ impl TryFrom<WSExecuteRequest> for (sandbox::ExecuteRequest, serde_json::Value)
6565
#[derive(Debug, serde::Serialize)]
6666
#[serde(tag = "type")]
6767
enum WSMessageResponse {
68+
#[serde(rename = "WEBSOCKET_ERROR")]
69+
Error(WSError),
6870
#[serde(rename = "WS_EXECUTE_RESPONSE")]
6971
WSExecuteResponse(WSExecuteResponse),
7072
}
7173

74+
#[derive(Debug, serde::Serialize)]
75+
#[serde(rename_all = "camelCase")]
76+
struct WSError {
77+
error: String,
78+
}
79+
7280
#[derive(Debug, serde::Serialize)]
7381
#[serde(rename_all = "camelCase")]
7482
struct WSExecuteResponse {
@@ -115,13 +123,14 @@ pub async fn handle(mut socket: WebSocket) {
115123
// unknown message type
116124
continue;
117125
}
118-
Some(Err(_)) => panic!("Error: {:?}", request),
126+
Some(Err(e)) => super::record_websocket_error(e.to_string()),
119127
}
120128
},
121129
resp = rx.recv() => {
122130
let resp = resp.expect("The rx should never close as we have a tx");
123-
let resp = resp.expect("An error occurred and should be reported back to the frontend");
124-
let resp = serde_json::to_string(&resp).expect("An error occurred and should be reported back to the frontend");
131+
let resp = resp.unwrap_or_else(|e| WSMessageResponse::Error(WSError { error: e.to_string() }));
132+
const LAST_CHANCE_ERROR: &str = r#"{ "type": "WEBSOCKET_ERROR", "error": "Unable to serialize JSON" }"#;
133+
let resp = serde_json::to_string(&resp).unwrap_or_else(|_| LAST_CHANCE_ERROR.into());
125134
let resp = Message::Text(resp);
126135

127136
if let Err(_) = socket.send(resp).await {

0 commit comments

Comments
 (0)