Skip to content

Commit 4bfabd6

Browse files
thomasballingerConvex, Inc.
authored andcommitted
Log relevant network issues in browser console (#40518)
As long as we're console.logging reconnections let's give more info. These logs might require `verbose: true` in the future but while there's here let's make them more useful. The guidelines added are relative; in an internal application blocking other server updates on 50MB of data may be no problem at all, while for mobile applications 5MB might feel unresponsive. GitOrigin-RevId: 0f19825798ff47e3bd4dd24e8a4ffe24deb8c59a
1 parent ec39923 commit 4bfabd6

File tree

2 files changed

+73
-7
lines changed

2 files changed

+73
-7
lines changed

npm-packages/convex/src/browser/sync/client.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,6 @@ export class BaseConvexClient {
396396
type: "Connect",
397397
sessionId: this._sessionId,
398398
maxObservedTimestamp: this.maxObservedTimestamp,
399-
clientTs: Date.now(),
400399
});
401400

402401
// Throw out our remote query, reissue queries

npm-packages/convex/src/browser/sync/web_socket_manager.ts

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,28 @@ type Socket =
8787
export type ReconnectMetadata = {
8888
connectionCount: number;
8989
lastCloseReason: string | null;
90+
clientTs: number;
9091
};
9192

9293
export type OnMessageResponse = {
9394
hasSyncedPastLastReconnect: boolean;
9495
};
9596

97+
let firstTime: number | undefined;
98+
function monotonicMillis() {
99+
if (firstTime === undefined) {
100+
firstTime = Date.now();
101+
}
102+
if (typeof performance === "undefined" || !performance.now) {
103+
return Date.now();
104+
}
105+
return Math.round(firstTime + performance.now());
106+
}
107+
108+
function prettyNow() {
109+
return `t=${Math.round((monotonicMillis() - firstTime!) / 100) / 10}s`;
110+
}
111+
96112
const serverDisconnectErrors = {
97113
// A known error, e.g. during a restart or push
98114
InternalServerError: { timeout: 1000 },
@@ -264,11 +280,21 @@ export class WebSocketManager {
264280
this.onOpen({
265281
connectionCount: this.connectionCount,
266282
lastCloseReason: this.lastCloseReason,
283+
clientTs: monotonicMillis(),
267284
});
268285
}
269286

270287
if (this.lastCloseReason !== "InitialConnect") {
271-
this.logger.log("WebSocket reconnected");
288+
if (this.lastCloseReason) {
289+
this.logger.log(
290+
"WebSocket reconnected at",
291+
prettyNow(),
292+
"after disconnect due to",
293+
this.lastCloseReason,
294+
);
295+
} else {
296+
this.logger.log("WebSocket reconnected at", prettyNow());
297+
}
272298
}
273299

274300
this.connectionCount += 1;
@@ -277,12 +303,50 @@ export class WebSocketManager {
277303
// NB: The WebSocket API calls `onclose` even if connection fails, so we can route all error paths through `onclose`.
278304
ws.onerror = (error) => {
279305
const message = (error as ErrorEvent).message;
280-
this.logger.log(`WebSocket error: ${message}`);
306+
if (message) {
307+
this.logger.log(`WebSocket error message: ${message}`);
308+
}
281309
};
282310
ws.onmessage = (message) => {
283311
this.resetServerInactivityTimeout();
312+
const messageLength = message.data.length;
284313
const serverMessage = parseServerMessage(JSON.parse(message.data));
285314
this._logVerbose(`received ws message with type ${serverMessage.type}`);
315+
if (
316+
serverMessage.type === "Transition" &&
317+
serverMessage.clientClockSkew !== undefined &&
318+
serverMessage.serverTs !== undefined
319+
) {
320+
const transitionTransitTime =
321+
monotonicMillis() - // client time now
322+
// clientClockSkew = (server time + upstream latency) - client time
323+
// clientClockSkew is "how many milliseconds behind (slow) is the client clock"
324+
// but the latency of the Connect message inflates this, making it appear further behind
325+
serverMessage.clientClockSkew -
326+
serverMessage.serverTs / 1_000_000; // server time when transition was sent
327+
const prettyTransitionTime = `${Math.round(transitionTransitTime)}ms`;
328+
const prettyMessageMB = `${Math.round(messageLength / 10_000) / 100}MB`;
329+
const bytesPerSecond = messageLength / (transitionTransitTime / 1000);
330+
const prettyBytesPerSecond = `${Math.round(bytesPerSecond / 10_000) / 100}MB per second`;
331+
this._logVerbose(
332+
`received ${prettyMessageMB} transition in ${prettyTransitionTime} at ${prettyBytesPerSecond}`,
333+
);
334+
335+
// Warnings that will show up for *all users*, so these are not very aggressive goals.
336+
if (transitionTransitTime > 10_000 && messageLength > 10_000_000) {
337+
this.logger.log(
338+
`received query results totalling more than 10MB (${prettyMessageMB}) which took more than 10s (${prettyTransitionTime}) to arrive`,
339+
);
340+
} else if (messageLength > 20_000_000) {
341+
this.logger.log(
342+
`received query results totalling more that 20MB (${prettyMessageMB}) which will take a long time to download on slower connections`,
343+
);
344+
} else if (transitionTransitTime > 20_000) {
345+
this.logger.log(
346+
`received query results totalling ${prettyMessageMB} which took more than 20s to arrive (${prettyTransitionTime})`,
347+
);
348+
}
349+
}
286350
const response = this.onMessage(serverMessage);
287351
if (response.hasSyncedPastLastReconnect) {
288352
// Reset backoff to 0 once all outstanding requests are complete.
@@ -293,7 +357,8 @@ export class WebSocketManager {
293357
ws.onclose = (event) => {
294358
this._logVerbose("begin ws.onclose");
295359
if (this.lastCloseReason === null) {
296-
this.lastCloseReason = event.reason ?? "OnCloseInvoked";
360+
// event.reason is often an empty string
361+
this.lastCloseReason = event.reason || `closed with code ${event.code}`;
297362
}
298363
if (
299364
event.code !== CLOSE_NORMAL &&
@@ -342,17 +407,18 @@ export class WebSocketManager {
342407
if (this.socket.state === "ready" && this.socket.paused === "no") {
343408
const encodedMessage = encodeClientMessage(message);
344409
const request = JSON.stringify(encodedMessage);
410+
let sent = false;
345411
try {
346412
this.socket.ws.send(request);
413+
sent = true;
347414
} catch (error: any) {
348415
this.logger.log(
349416
`Failed to send message on WebSocket, reconnecting: ${error}`,
350417
);
351418
this.closeAndReconnect("FailedToSendMessage");
352419
}
353-
// We are not sure if this was sent or not.
354420
this._logVerbose(
355-
`sent message with type ${message.type}: ${JSON.stringify(
421+
`${sent ? "sent" : "failed to send"} message with type ${message.type}: ${JSON.stringify(
356422
messageForLog,
357423
)}`,
358424
);
@@ -385,7 +451,7 @@ export class WebSocketManager {
385451
this.socket = { state: "disconnected" };
386452
const backoff = this.nextBackoff(reason);
387453
this.markConnectionStateDirty();
388-
this.logger.log(`Attempting reconnect in ${backoff}ms`);
454+
this.logger.log(`Attempting reconnect in ${Math.round(backoff)}ms`);
389455
setTimeout(() => this.connect(), backoff);
390456
}
391457

@@ -568,6 +634,7 @@ export class WebSocketManager {
568634
this.onOpen({
569635
connectionCount: this.connectionCount,
570636
lastCloseReason: this.lastCloseReason,
637+
clientTs: monotonicMillis(),
571638
});
572639
} else if (this.socket.paused === "yes") {
573640
this.socket = { ...this.socket, paused: "no" };

0 commit comments

Comments
 (0)