Skip to content

Commit 681bbbd

Browse files
sshaderConvex, Inc.
authored andcommitted
Add a WS connection test in npx convex network-test (#34611)
To help debug cases where folks can't connect to their deployments and it might have to do with local network. This uses `BaseConvexClient` to open a web socket and call a system UDF that just returns the URL. Because it's a system UDF, we only test this if we can get an admin key / access token. The more verbose logs are gated with `CONVEX_VERBOSE`. GitOrigin-RevId: 524508460eccf1cb064bec4420c79127787b4ff5
1 parent ca221a3 commit 681bbbd

File tree

3 files changed

+99
-15
lines changed

3 files changed

+99
-15
lines changed

npm-packages/convex/src/bundler/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ export function logError(ctx: Context, message: string) {
127127
}
128128

129129
// Handles clearing spinner so that it doesn't get messed up
130-
export function logWarning(ctx: Context, message: string) {
130+
export function logWarning(ctx: Context, ...logged: any) {
131131
ctx.spinner?.clear();
132-
logToStderr(message);
132+
logToStderr(...logged);
133133
}
134134

135135
// Handles clearing spinner so that it doesn't get messed up

npm-packages/convex/src/cli/lib/networkTest.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
logFailure,
44
logFinishedStep,
55
logMessage,
6+
logVerbose,
7+
logWarning,
68
} from "../../bundler/context.js";
79
import chalk from "chalk";
810
import * as net from "net";
@@ -14,13 +16,15 @@ import {
1416
formatSize,
1517
ThrowingFetchError,
1618
} from "./utils/utils.js";
17-
19+
import ws from "ws";
20+
import { BaseConvexClient } from "../../browser/index.js";
21+
import { Logger } from "../../browser/logging.js";
1822
const ipFamilyNumbers = { ipv4: 4, ipv6: 6, auto: 0 } as const;
1923
const ipFamilyNames = { 4: "ipv4", 6: "ipv6", 0: "auto" } as const;
2024

2125
export async function runNetworkTestOnUrl(
2226
ctx: Context,
23-
url: string,
27+
{ url, adminKey }: { url: string; adminKey: string | null },
2428
options: {
2529
ipFamily?: string;
2630
speedTest?: boolean;
@@ -32,9 +36,12 @@ export async function runNetworkTestOnUrl(
3236
// Second, check to see if we can open a TCP connection to the hostname.
3337
await checkTcp(ctx, url, options.ipFamily ?? "auto");
3438

35-
// Fourth, do a simple HTTPS request and check that we receive a 200.
39+
// Third, do a simple HTTPS request and check that we receive a 200.
3640
await checkHttp(ctx, url);
3741

42+
// Fourth, check that we can open a WebSocket connection to the hostname.
43+
await checkWs(ctx, { url, adminKey });
44+
3845
// Fifth, check a small echo request, much smaller than most networks' MTU.
3946
await checkEcho(ctx, url, 128);
4047

@@ -188,6 +195,80 @@ async function checkHttpOnce(
188195
);
189196
}
190197

198+
async function checkWs(
199+
ctx: Context,
200+
{ url, adminKey }: { url: string; adminKey: string | null },
201+
) {
202+
if (adminKey === null) {
203+
logWarning(
204+
ctx,
205+
"Skipping WebSocket check because no admin key was provided.",
206+
);
207+
return;
208+
}
209+
let queryPromiseResolver: ((value: string) => void) | null = null;
210+
const queryPromise = new Promise<string | null>((resolve) => {
211+
queryPromiseResolver = resolve;
212+
});
213+
const logger = new Logger({
214+
verbose: process.env.CONVEX_VERBOSE !== undefined,
215+
});
216+
logger.addLogLineListener((level, ...args) => {
217+
switch (level) {
218+
case "debug":
219+
logVerbose(ctx, ...args);
220+
break;
221+
case "info":
222+
logVerbose(ctx, ...args);
223+
break;
224+
case "warn":
225+
logWarning(ctx, ...args);
226+
break;
227+
case "error":
228+
// TODO: logFailure is a little hard to use here because it also interacts
229+
// with the spinner and requires a string.
230+
logWarning(ctx, ...args);
231+
break;
232+
}
233+
});
234+
const convexClient = new BaseConvexClient(
235+
url,
236+
(updatedQueries) => {
237+
for (const queryToken of updatedQueries) {
238+
const result = convexClient.localQueryResultByToken(queryToken);
239+
if (typeof result === "string" && queryPromiseResolver !== null) {
240+
queryPromiseResolver(result);
241+
queryPromiseResolver = null;
242+
}
243+
}
244+
},
245+
{
246+
webSocketConstructor: ws as unknown as typeof WebSocket,
247+
unsavedChangesWarning: false,
248+
logger,
249+
},
250+
);
251+
convexClient.setAdminAuth(adminKey);
252+
convexClient.subscribe("_system/cli/convexUrl:cloudUrl", {});
253+
const racePromise = Promise.race([
254+
queryPromise,
255+
new Promise((resolve) => setTimeout(() => resolve(null), 10000)),
256+
]);
257+
const cloudUrl = await racePromise;
258+
if (cloudUrl === null) {
259+
return ctx.crash({
260+
exitCode: 1,
261+
errorType: "transient",
262+
printedMessage: "FAIL: Failed to connect to deployment over WebSocket.",
263+
});
264+
} else {
265+
logMessage(
266+
ctx,
267+
`${chalk.green(`✔`)} OK: WebSocket connection established.`,
268+
);
269+
}
270+
}
271+
191272
async function checkEcho(ctx: Context, url: string, size: number) {
192273
try {
193274
const start = performance.now();

npm-packages/convex/src/cli/network_test.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,29 +52,32 @@ async function runNetworkTest(
5252
ctx,
5353
options,
5454
);
55-
const url = await loadUrl(ctx, deploymentSelection);
56-
await runNetworkTestOnUrl(ctx, url, options);
55+
const { url, adminKey } = await loadUrlAndAdminKey(ctx, deploymentSelection);
56+
await runNetworkTestOnUrl(ctx, { url, adminKey }, options);
5757
}
5858

59-
async function loadUrl(
59+
async function loadUrlAndAdminKey(
6060
ctx: Context,
6161
deploymentSelection: DeploymentSelection,
62-
): Promise<string> {
62+
): Promise<{ url: string; adminKey: string | null }> {
6363
// Try to fetch the URL following the usual paths, but special case the
6464
// `--url` argument in case the developer doesn't have network connectivity.
6565
let url: string;
66-
if (
67-
deploymentSelection.kind === "urlWithAdminKey" ||
68-
deploymentSelection.kind === "urlWithLogin"
69-
) {
66+
let adminKey: string | null;
67+
if (deploymentSelection.kind === "urlWithAdminKey") {
7068
url = deploymentSelection.url;
69+
adminKey = deploymentSelection.adminKey;
70+
} else if (deploymentSelection.kind === "urlWithLogin") {
71+
url = deploymentSelection.url;
72+
adminKey = null;
7173
} else {
7274
const credentials = await fetchDeploymentCredentialsProvisionProd(
7375
ctx,
7476
deploymentSelection,
7577
);
7678
url = credentials.url;
79+
adminKey = credentials.adminKey;
7780
}
78-
logMessage(ctx, `${chalk.green(`✔`)} Project URL: ${url}`);
79-
return url;
81+
logMessage(ctx, `${chalk.green(`✔`)} Deployment URL: ${url}`);
82+
return { url, adminKey };
8083
}

0 commit comments

Comments
 (0)