From f50685b58cf365a9e3164fd1f1cb1ce437478c86 Mon Sep 17 00:00:00 2001 From: Sarah Shader Date: Wed, 9 Oct 2024 16:56:20 -0400 Subject: [PATCH] Use `logger` abstraction in CLI + client libraries (#30497) I'd like to make it easier to configure logging levels + some day attach listeners for log events. This adds a `Logger` class that all our logging goes through, and defaults to logging to the console (so identical behavior as before). GitOrigin-RevId: 8369e5ba460ec62d94f7cebea1cef3f2a8adb2ea --- npm-packages/convex/eslint.config.mjs | 3 + .../convex/src/browser/http_client.ts | 32 ++++-- npm-packages/convex/src/browser/logging.ts | 107 ++++++++++++++++-- .../browser/sync/authentication_manager.ts | 25 ++-- .../convex/src/browser/sync/client.ts | 38 ++++--- .../browser/sync/client_node_test_helpers.ts | 3 + .../src/browser/sync/remote_query_set.ts | 10 +- .../src/browser/sync/request_manager.test.ts | 5 +- .../src/browser/sync/request_manager.ts | 8 +- .../src/browser/sync/web_socket_manager.ts | 23 ++-- npm-packages/convex/src/bundler/context.ts | 2 + npm-packages/convex/src/bundler/fs.ts | 6 + npm-packages/convex/src/bundler/index.test.ts | 8 +- npm-packages/convex/src/bundler/index.ts | 83 ++++++-------- .../cli/codegen_templates/component_api.ts | 2 +- npm-packages/convex/src/cli/dashboard.ts | 4 +- npm-packages/convex/src/cli/deployments.ts | 9 +- npm-packages/convex/src/cli/dev.ts | 10 +- npm-packages/convex/src/cli/docs.ts | 12 +- npm-packages/convex/src/cli/index.ts | 2 + npm-packages/convex/src/cli/lib/codegen.ts | 2 +- .../cli/lib/components/definition/bundle.ts | 8 +- npm-packages/convex/src/cli/lib/config.ts | 2 +- npm-packages/convex/src/cli/lib/deploy2.ts | 33 ++---- npm-packages/convex/src/cli/lib/login.ts | 3 +- npm-packages/convex/src/cli/lib/push.ts | 3 + npm-packages/convex/src/cli/update.ts | 5 +- npm-packages/convex/src/react/client.ts | 16 ++- .../convex/src/react/use_paginated_query.ts | 6 +- 29 files changed, 306 insertions(+), 164 deletions(-) diff --git a/npm-packages/convex/eslint.config.mjs b/npm-packages/convex/eslint.config.mjs index 97a85c79..c8efa761 100644 --- a/npm-packages/convex/eslint.config.mjs +++ b/npm-packages/convex/eslint.config.mjs @@ -126,6 +126,9 @@ export default [ fixable: false, }, ], + + // Use `logMessage` and friends (CLI specific) or `logger.log` instead. + "no-console": "error", }, }, { diff --git a/npm-packages/convex/src/browser/http_client.ts b/npm-packages/convex/src/browser/http_client.ts index c8f091d1..87ad0f20 100644 --- a/npm-packages/convex/src/browser/http_client.ts +++ b/npm-packages/convex/src/browser/http_client.ts @@ -12,7 +12,7 @@ import { convexToJson, jsonToConvex, } from "../values/index.js"; -import { logToConsole } from "./logging.js"; +import { instantiateDefaultLogger, logForFunction, Logger } from "./logging.js"; import { FunctionArgs, UserIdentityAttributes } from "../server/index.js"; export const STATUS_CODE_OK = 200; @@ -46,20 +46,34 @@ export class ConvexHttpClient { private encodedTsPromise?: Promise; private debug: boolean; private fetchOptions?: FetchOptions; - + private logger: Logger; /** * Create a new {@link ConvexHttpClient}. * * @param address - The url of your Convex deployment, often provided * by an environment variable. E.g. `https://small-mouse-123.convex.cloud`. - * @param skipConvexDeploymentUrlCheck - Skip validating that the Convex deployment URL looks like + * @param options - An object of options. + * - `skipConvexDeploymentUrlCheck` - Skip validating that the Convex deployment URL looks like * `https://happy-animal-123.convex.cloud` or localhost. This can be useful if running a self-hosted * Convex backend that uses a different URL. + * - `logger` - A logger. If not provided, logs to the console. + * You can construct your own logger to customize logging to log elsewhere + * or not log at all. */ - constructor(address: string, skipConvexDeploymentUrlCheck?: boolean) { - if (skipConvexDeploymentUrlCheck !== true) { + constructor( + address: string, + options?: { skipConvexDeploymentUrlCheck?: boolean; logger?: Logger }, + ) { + if (typeof options === "boolean") { + throw new Error( + "skipConvexDeploymentUrlCheck as the second argument is no longer supported. Please pass an options object, `{ skipConvexDeploymentUrlCheck: true }`.", + ); + } + const opts = options ?? {}; + if (opts.skipConvexDeploymentUrlCheck !== true) { validateDeploymentUrl(address); } + this.logger = opts.logger ?? instantiateDefaultLogger({ verbose: false }); this.address = address; this.debug = true; } @@ -255,7 +269,7 @@ export class ConvexHttpClient { if (this.debug) { for (const line of respJSON.logLines ?? []) { - logToConsole("info", "query", name, line); + logForFunction(this.logger, "info", "query", name, line); } } switch (respJSON.status) { @@ -316,7 +330,7 @@ export class ConvexHttpClient { const respJSON = await response.json(); if (this.debug) { for (const line of respJSON.logLines ?? []) { - logToConsole("info", "mutation", name, line); + logForFunction(this.logger, "info", "mutation", name, line); } } switch (respJSON.status) { @@ -377,7 +391,7 @@ export class ConvexHttpClient { const respJSON = await response.json(); if (this.debug) { for (const line of respJSON.logLines ?? []) { - logToConsole("info", "action", name, line); + logForFunction(this.logger, "info", "action", name, line); } } switch (respJSON.status) { @@ -447,7 +461,7 @@ export class ConvexHttpClient { const respJSON = await response.json(); if (this.debug) { for (const line of respJSON.logLines ?? []) { - logToConsole("info", "any", name, line); + logForFunction(this.logger, "info", "any", name, line); } } switch (respJSON.status) { diff --git a/npm-packages/convex/src/browser/logging.ts b/npm-packages/convex/src/browser/logging.ts index ff7c0f5e..be457019 100644 --- a/npm-packages/convex/src/browser/logging.ts +++ b/npm-packages/convex/src/browser/logging.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ // This is the one file where we can `console.log` for the default logger implementation. import { ConvexError, Value } from "../values/index.js"; import { FunctionFailure } from "./sync/function_result.js"; @@ -20,7 +21,97 @@ function prefix_for_source(source: UdfType) { } } -export function logToConsole( +export type LogLevel = "debug" | "info" | "warn" | "error"; + +/** + * A logger that can be used to log messages. By default, this is a wrapper + * around `console`, but can be configured to not log at all or to log somewhere + * else. + */ +export class Logger { + private _onLogLineFuncs: Record< + string, + (level: LogLevel, ...args: any[]) => void + >; + private _verbose: boolean; + + constructor(options: { verbose: boolean }) { + this._onLogLineFuncs = {}; + this._verbose = options.verbose; + } + + addLogLineListener( + func: (level: LogLevel, ...args: any[]) => void, + ): () => void { + let id = Math.random().toString(36).substring(2, 15); + for (let i = 0; i < 10; i++) { + if (this._onLogLineFuncs[id] === undefined) { + break; + } + id = Math.random().toString(36).substring(2, 15); + } + this._onLogLineFuncs[id] = func; + return () => { + delete this._onLogLineFuncs[id]; + }; + } + + logVerbose(...args: any[]) { + if (this._verbose) { + for (const func of Object.values(this._onLogLineFuncs)) { + func("debug", `${new Date().toISOString()}`, ...args); + } + } + } + + log(...args: any[]) { + for (const func of Object.values(this._onLogLineFuncs)) { + func("info", ...args); + } + } + + warn(...args: any[]) { + for (const func of Object.values(this._onLogLineFuncs)) { + func("warn", ...args); + } + } + + error(...args: any[]) { + for (const func of Object.values(this._onLogLineFuncs)) { + func("error", ...args); + } + } +} + +export function instantiateDefaultLogger(options: { + verbose: boolean; +}): Logger { + const logger = new Logger(options); + logger.addLogLineListener((level, ...args) => { + switch (level) { + case "debug": + console.debug(...args); + break; + case "info": + console.log(...args); + break; + case "warn": + console.warn(...args); + break; + case "error": + console.error(...args); + break; + default: { + const _typecheck: never = level; + console.log(...args); + } + } + }); + return logger; +} + +export function logForFunction( + logger: Logger, type: "info" | "error", source: UdfType, udfPath: string, @@ -34,7 +125,7 @@ export function logToConsole( if (type === "info") { const match = message.match(/^\[.*?\] /); if (match === null) { - console.error( + logger.error( `[CONVEX ${prefix}(${udfPath})] Could not parse console.log`, ); return; @@ -42,19 +133,15 @@ export function logToConsole( const level = message.slice(1, match[0].length - 2); const args = message.slice(match[0].length); - console.log( - `%c[CONVEX ${prefix}(${udfPath})] [${level}]`, - INFO_COLOR, - args, - ); + logger.log(`%c[CONVEX ${prefix}(${udfPath})] [${level}]`, INFO_COLOR, args); } else { - console.error(`[CONVEX ${prefix}(${udfPath})] ${message}`); + logger.error(`[CONVEX ${prefix}(${udfPath})] ${message}`); } } -export function logFatalError(message: string): Error { +export function logFatalError(logger: Logger, message: string): Error { const errorMessage = `[CONVEX FATAL ERROR] ${message}`; - console.error(errorMessage); + logger.error(errorMessage); return new Error(errorMessage); } diff --git a/npm-packages/convex/src/browser/sync/authentication_manager.ts b/npm-packages/convex/src/browser/sync/authentication_manager.ts index 71165fb2..a5df3e40 100644 --- a/npm-packages/convex/src/browser/sync/authentication_manager.ts +++ b/npm-packages/convex/src/browser/sync/authentication_manager.ts @@ -1,3 +1,4 @@ +import { Logger } from "../logging.js"; import { LocalSyncState } from "./local_state.js"; import { AuthError, Transition } from "./protocol.js"; import { jwtDecode } from "jwt-decode"; @@ -89,7 +90,7 @@ export class AuthenticationManager { private readonly resumeSocket: () => void; // Passed down by BaseClient, sends a message to the server private readonly clearAuth: () => void; - private readonly verbose: boolean; + private readonly logger: Logger; constructor( syncState: LocalSyncState, @@ -100,7 +101,7 @@ export class AuthenticationManager { pauseSocket, resumeSocket, clearAuth, - verbose, + logger, }: { authenticate: (token: string) => void; stopSocket: () => Promise; @@ -108,7 +109,7 @@ export class AuthenticationManager { pauseSocket: () => void; resumeSocket: () => void; clearAuth: () => void; - verbose: boolean; + logger: Logger; }, ) { this.syncState = syncState; @@ -118,7 +119,7 @@ export class AuthenticationManager { this.pauseSocket = pauseSocket; this.resumeSocket = resumeSocket; this.clearAuth = clearAuth; - this.verbose = verbose; + this.logger = logger; } async setConfig( @@ -216,7 +217,7 @@ export class AuthenticationManager { // We failed on a fresh token, trying another one won't help this.authState.state === "waitingForServerConfirmationOfFreshToken" ) { - console.error( + this.logger.error( `Failed to authenticate: "${serverMessage.error}", check your server auth config`, ); if (this.syncState.hasAuth()) { @@ -316,14 +317,16 @@ export class AuthenticationManager { // This is no longer really possible, because // we wait on server response before scheduling token refetch, // and the server currently requires JWT tokens. - console.error("Auth token is not a valid JWT, cannot refetch the token"); + this.logger.error( + "Auth token is not a valid JWT, cannot refetch the token", + ); return; } // iat: issued at time, UTC seconds timestamp at which the JWT was issued // exp: expiration time, UTC seconds timestamp at which the JWT will expire const { iat, exp } = decodedToken as { iat?: number; exp?: number }; if (!iat || !exp) { - console.error( + this.logger.error( "Auth token does not have required fields, cannot refetch the token", ); return; @@ -338,7 +341,7 @@ export class AuthenticationManager { (exp - iat - leewaySeconds) * 1000, ); if (delay <= 0) { - console.error( + this.logger.error( "Auth token does not live long enough, cannot refetch the token", ); return; @@ -413,10 +416,6 @@ export class AuthenticationManager { } private _logVerbose(message: string) { - if (this.verbose) { - console.debug( - `${new Date().toISOString()} ${message} [v${this.configVersion}]`, - ); - } + this.logger.logVerbose(`${message} [v${this.configVersion}]`); } } diff --git a/npm-packages/convex/src/browser/sync/client.ts b/npm-packages/convex/src/browser/sync/client.ts index d4b183f6..5b9f3429 100644 --- a/npm-packages/convex/src/browser/sync/client.ts +++ b/npm-packages/convex/src/browser/sync/client.ts @@ -3,7 +3,9 @@ import { convexToJson, Value } from "../../values/index.js"; import { createHybridErrorStacktrace, forwardData, + instantiateDefaultLogger, logFatalError, + Logger, } from "../logging.js"; import { LocalSyncState } from "./local_state.js"; import { RequestManager } from "./request_manager.js"; @@ -67,6 +69,13 @@ export interface BaseConvexClientOptions { * The default value is `false`. */ verbose?: boolean; + /** + * A logger. If not provided, logs to the console. + * + * You can construct your own logger to customize logging to log elsewhere + * or not log at all. + */ + logger?: Logger; /** * Sends additional metrics to Convex for debugging purposes. * @@ -153,8 +162,8 @@ export class BaseConvexClient { private _nextRequestId: RequestId; private readonly _sessionId: string; private firstMessageReceived = false; - private readonly verbose: boolean; private readonly debug: boolean; + private readonly logger: Logger; private maxObservedTimestamp: TS | undefined; /** @@ -185,10 +194,11 @@ export class BaseConvexClient { ); } webSocketConstructor = webSocketConstructor || WebSocket; - this.verbose = options.verbose ?? false; this.debug = options.reportDebugInfoToConvex ?? false; this.address = address; - + this.logger = + options.logger ?? + instantiateDefaultLogger({ verbose: options.verbose ?? false }); // Substitute http(s) with ws(s) const i = address.search("://"); if (i === -1) { @@ -207,10 +217,11 @@ export class BaseConvexClient { const wsUri = `${wsProtocol}://${origin}/api/${version}/sync`; this.state = new LocalSyncState(); - this.remoteQuerySet = new RemoteQuerySet((queryId) => - this.state.queryPath(queryId), + this.remoteQuerySet = new RemoteQuerySet( + (queryId) => this.state.queryPath(queryId), + this.logger, ); - this.requestManager = new RequestManager(); + this.requestManager = new RequestManager(this.logger); this.authenticationManager = new AuthenticationManager(this.state, { authenticate: (token) => { const message = this.state.setAuth(token); @@ -226,7 +237,7 @@ export class BaseConvexClient { clearAuth: () => { this.clearAuth(); }, - verbose: this.verbose, + logger: this.logger, }); this.optimisticQueryResults = new OptimisticQueryResults(); this.onTransition = onTransition; @@ -279,8 +290,9 @@ export class BaseConvexClient { const oldRemoteQueryResults = new Set( this.remoteQuerySet.remoteQueryResults().keys(), ); - this.remoteQuerySet = new RemoteQuerySet((queryId) => - this.state.queryPath(queryId), + this.remoteQuerySet = new RemoteQuerySet( + (queryId) => this.state.queryPath(queryId), + this.logger, ); const [querySetModification, authModification] = this.state.restart( oldRemoteQueryResults, @@ -345,7 +357,7 @@ export class BaseConvexClient { break; } case "FatalError": { - const error = logFatalError(serverMessage.error); + const error = logFatalError(this.logger, serverMessage.error); void this.webSocketManager.terminate(); throw error; } @@ -362,7 +374,7 @@ export class BaseConvexClient { }, }, webSocketConstructor, - this.verbose, + this.logger, ); this.mark("convexClientConstructed"); } @@ -781,14 +793,14 @@ export class BaseConvexClient { }) .then((response) => { if (!response.ok) { - console.warn( + this.logger.warn( "Analytics request failed with response:", response.body, ); } }) .catch((error) => { - console.warn("Analytics response failed with error:", error); + this.logger.warn("Analytics response failed with error:", error); }); } } diff --git a/npm-packages/convex/src/browser/sync/client_node_test_helpers.ts b/npm-packages/convex/src/browser/sync/client_node_test_helpers.ts index 3d1139ae..d7c8939c 100644 --- a/npm-packages/convex/src/browser/sync/client_node_test_helpers.ts +++ b/npm-packages/convex/src/browser/sync/client_node_test_helpers.ts @@ -42,6 +42,7 @@ export async function withInMemoryWebSocket( socket = ws; ws.on("message", function message(data: string) { received(data); + // eslint-disable-next-line no-console if (debug) console.debug(`client --${JSON.parse(data).type}--> `); messages.push( new Promise((r) => { @@ -62,6 +63,7 @@ export async function withInMemoryWebSocket( return JSON.parse(await msgP); } function send(message: ServerMessage) { + // eslint-disable-next-line no-console if (debug) console.debug(` <--${message.type}-- server`); socket!.send(encodeServerMessage(message)); } @@ -76,6 +78,7 @@ export async function withInMemoryWebSocket( receive, send, close: () => { + // eslint-disable-next-line no-console if (debug) console.debug(` -->8-CLOSE- server`); socket!.close(); setUpSocket(); diff --git a/npm-packages/convex/src/browser/sync/remote_query_set.ts b/npm-packages/convex/src/browser/sync/remote_query_set.ts index 7ee6c543..e63ff6f3 100644 --- a/npm-packages/convex/src/browser/sync/remote_query_set.ts +++ b/npm-packages/convex/src/browser/sync/remote_query_set.ts @@ -1,6 +1,6 @@ import { jsonToConvex } from "../../values/index.js"; import { Long } from "../long.js"; -import { logToConsole } from "../logging.js"; +import { logForFunction, Logger } from "../logging.js"; import { QueryId, StateVersion, Transition } from "./protocol.js"; import { FunctionResult } from "./function_result.js"; @@ -12,11 +12,13 @@ export class RemoteQuerySet { private version: StateVersion; private readonly remoteQuerySet: Map; private readonly queryPath: (queryId: QueryId) => string | null; + private readonly logger: Logger; - constructor(queryPath: (queryId: QueryId) => string | null) { + constructor(queryPath: (queryId: QueryId) => string | null, logger: Logger) { this.version = { querySet: 0, ts: Long.fromNumber(0), identity: 0 }; this.remoteQuerySet = new Map(); this.queryPath = queryPath; + this.logger = logger; } transition(transition: Transition): void { @@ -36,7 +38,7 @@ export class RemoteQuerySet { const queryPath = this.queryPath(modification.queryId); if (queryPath) { for (const line of modification.logLines) { - logToConsole("info", "query", queryPath, line); + logForFunction(this.logger, "info", "query", queryPath, line); } } const value = jsonToConvex(modification.value ?? null); @@ -51,7 +53,7 @@ export class RemoteQuerySet { const queryPath = this.queryPath(modification.queryId); if (queryPath) { for (const line of modification.logLines) { - logToConsole("info", "query", queryPath, line); + logForFunction(this.logger, "info", "query", queryPath, line); } } const { errorData } = modification; diff --git a/npm-packages/convex/src/browser/sync/request_manager.test.ts b/npm-packages/convex/src/browser/sync/request_manager.test.ts index 98867913..27e33157 100644 --- a/npm-packages/convex/src/browser/sync/request_manager.test.ts +++ b/npm-packages/convex/src/browser/sync/request_manager.test.ts @@ -2,11 +2,14 @@ import { test, expect, beforeEach } from "vitest"; import { RequestManager } from "./request_manager.js"; import { Long } from "../long.js"; import { ActionRequest, MutationRequest } from "./protocol.js"; +import { instantiateDefaultLogger } from "../logging.js"; let requestManager: RequestManager; beforeEach(() => { - requestManager = new RequestManager(); + requestManager = new RequestManager( + instantiateDefaultLogger({ verbose: false }), + ); }); test("hasIncompleteRequests", () => { diff --git a/npm-packages/convex/src/browser/sync/request_manager.ts b/npm-packages/convex/src/browser/sync/request_manager.ts index e74eeea8..f152887e 100644 --- a/npm-packages/convex/src/browser/sync/request_manager.ts +++ b/npm-packages/convex/src/browser/sync/request_manager.ts @@ -1,5 +1,5 @@ import { jsonToConvex } from "../../values/index.js"; -import { logToConsole } from "../logging.js"; +import { logForFunction, Logger } from "../logging.js"; import { Long } from "../long.js"; import { FunctionResult } from "./function_result.js"; import { @@ -32,7 +32,7 @@ export class RequestManager { } >; private requestsOlderThanRestart: Set; - constructor() { + constructor(private readonly logger: Logger) { this.inflightRequests = new Map(); this.requestsOlderThanRestart = new Set(); } @@ -92,7 +92,7 @@ export class RequestManager { const udfPath = requestInfo.message.udfPath; for (const line of response.logLines) { - logToConsole("info", udfType, udfPath, line); + logForFunction(this.logger, "info", udfType, udfPath, line); } const status = requestInfo.status; @@ -107,7 +107,7 @@ export class RequestManager { } else { const errorMessage = response.result as string; const { errorData } = response; - logToConsole("error", udfType, udfPath, errorMessage); + logForFunction(this.logger, "error", udfType, udfPath, errorMessage); onResolve = () => status.onResult({ success: false, diff --git a/npm-packages/convex/src/browser/sync/web_socket_manager.ts b/npm-packages/convex/src/browser/sync/web_socket_manager.ts index 31881560..029f24f7 100644 --- a/npm-packages/convex/src/browser/sync/web_socket_manager.ts +++ b/npm-packages/convex/src/browser/sync/web_socket_manager.ts @@ -1,3 +1,4 @@ +import { Logger } from "../logging.js"; import { ClientMessage, encodeClientMessage, @@ -124,7 +125,7 @@ export class WebSocketManager { private readonly onResume: () => void; private readonly onMessage: (message: ServerMessage) => OnMessageResponse; private readonly webSocketConstructor: typeof WebSocket; - private readonly verbose: boolean; + private readonly logger: Logger; constructor( uri: string, @@ -134,7 +135,7 @@ export class WebSocketManager { onMessage: (message: ServerMessage) => OnMessageResponse; }, webSocketConstructor: typeof WebSocket, - verbose: boolean, + logger: Logger, ) { this.webSocketConstructor = webSocketConstructor; this.socket = { state: "disconnected" }; @@ -152,7 +153,7 @@ export class WebSocketManager { this.onOpen = callbacks.onOpen; this.onResume = callbacks.onResume; this.onMessage = callbacks.onMessage; - this.verbose = verbose; + this.logger = logger; this.connect(); } @@ -185,7 +186,7 @@ export class WebSocketManager { this.resetServerInactivityTimeout(); ws.onopen = () => { - this._logVerbose("begin ws.onopen"); + this.logger.logVerbose("begin ws.onopen"); if (this.socket.state !== "connecting") { throw new Error("onopen called with socket not in connecting state"); } @@ -203,7 +204,7 @@ export class WebSocketManager { } if (this.lastCloseReason !== "InitialConnect") { - console.log("WebSocket reconnected"); + this.logger.log("WebSocket reconnected"); } this.connectionCount += 1; @@ -212,7 +213,7 @@ export class WebSocketManager { // NB: The WebSocket API calls `onclose` even if connection fails, so we can route all error paths through `onclose`. ws.onerror = (error) => { const message = (error as ErrorEvent).message; - console.log(`WebSocket error: ${message}`); + this.logger.log(`WebSocket error: ${message}`); }; ws.onmessage = (message) => { this.resetServerInactivityTimeout(); @@ -239,7 +240,7 @@ export class WebSocketManager { if (event.reason) { msg += `: ${event.reason}`; } - console.log(msg); + this.logger.log(msg); } this.scheduleReconnect(); return; @@ -266,7 +267,7 @@ export class WebSocketManager { try { this.socket.ws.send(request); } catch (error: any) { - console.log( + this.logger.log( `Failed to send message on WebSocket, reconnecting: ${error}`, ); this.closeAndReconnect("FailedToSendMessage"); @@ -294,7 +295,7 @@ export class WebSocketManager { private scheduleReconnect() { this.socket = { state: "disconnected" }; const backoff = this.nextBackoff(); - console.log(`Attempting reconnect in ${backoff}ms`); + this.logger.log(`Attempting reconnect in ${backoff}ms`); setTimeout(() => this.connect(), backoff); } @@ -498,9 +499,7 @@ export class WebSocketManager { } private _logVerbose(message: string) { - if (this.verbose) { - console.debug(`${new Date().toISOString()} ${message}`); - } + this.logger.logVerbose(message); } private nextBackoff(): number { diff --git a/npm-packages/convex/src/bundler/context.ts b/npm-packages/convex/src/bundler/context.ts index 9f936ebe..ccd6ea67 100644 --- a/npm-packages/convex/src/bundler/context.ts +++ b/npm-packages/convex/src/bundler/context.ts @@ -132,6 +132,8 @@ export function logMessage(ctx: Context, ...logged: any) { // (logMesage, logWarning, etc.) should be written to stderr. export function logOutput(ctx: Context, ...logged: any) { ctx.spinner?.clear(); + // the one spot where we can console.log + // eslint-disable-next-line no-console console.log(...logged); } diff --git a/npm-packages/convex/src/bundler/fs.ts b/npm-packages/convex/src/bundler/fs.ts index 871acee9..c62d5cb3 100644 --- a/npm-packages/convex/src/bundler/fs.ts +++ b/npm-packages/convex/src/bundler/fs.ts @@ -33,11 +33,14 @@ let warned = false; function warnCrossFilesystem(dstPath: string) { const dstDir = path.dirname(dstPath); if (!warned) { + // It's hard for these to use `logMessage` without creating a circular dependency, so just log directly. + // eslint-disable-next-line no-console console.warn( chalk.yellow( `Temporary directory '${tmpDirRoot}' and project directory '${dstDir}' are on different filesystems.`, ), ); + // eslint-disable-next-line no-console console.warn( chalk.gray( ` If you're running into errors with other tools watching the project directory, override the temporary directory location with the ${chalk.bold( @@ -45,6 +48,7 @@ function warnCrossFilesystem(dstPath: string) { )} environment variable.`, ), ); + // eslint-disable-next-line no-console console.warn( chalk.gray( ` Be sure to pick a temporary directory that's on the same filesystem as your project.`, @@ -246,6 +250,7 @@ export class RecordingFs implements Filesystem { if (existingNames) { if (!setsEqual(observedNames, existingNames)) { if (this.traceEvents) { + // eslint-disable-next-line no-console console.log( "Invalidating due to directory children mismatch", observedNames, @@ -409,6 +414,7 @@ export class RecordingFs implements Filesystem { const stMatch = stMatches(observed, existing); if (!stMatch.matches) { if (this.traceEvents) { + // eslint-disable-next-line no-console console.log( "Invalidating due to st mismatch", absPath, diff --git a/npm-packages/convex/src/bundler/index.test.ts b/npm-packages/convex/src/bundler/index.test.ts index ce17fe43..0b70c8cd 100644 --- a/npm-packages/convex/src/bundler/index.test.ts +++ b/npm-packages/convex/src/bundler/index.test.ts @@ -36,7 +36,7 @@ test("bundle function is present", () => { test("bundle finds JavaScript functions", async () => { const fixtureDir = dirname + "/test_fixtures/js/project01"; const ctx = oneoffContext(); - const entryPoints = await entryPointsByEnvironment(ctx, fixtureDir, false); + const entryPoints = await entryPointsByEnvironment(ctx, fixtureDir); const bundles = sorted( (await bundle(ctx, fixtureDir, entryPoints.isolate, false, "browser")) .modules, @@ -106,7 +106,7 @@ test("returns true when multiple imports and httpRouter is imported", async () = test("bundle warns about https.js|ts at top level", async () => { const fixtureDir = dirname + "/test_fixtures/js/project_with_https"; const logSpy = vi.spyOn(process.stderr, "write"); - await entryPoints(oneoffContext(), fixtureDir, false); + await entryPoints(oneoffContext(), fixtureDir); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("https")); }); @@ -114,7 +114,7 @@ test("bundle does not warn about https.js|ts which is not at top level", async ( const fixtureDir = dirname + "/test_fixtures/js/project_with_https_not_at_top_level"; const logSpy = vi.spyOn(process.stderr, "write"); - await entryPoints(oneoffContext(), fixtureDir, false); + await entryPoints(oneoffContext(), fixtureDir); expect(logSpy).toHaveBeenCalledTimes(0); }); @@ -122,7 +122,7 @@ test("bundle does not warn about https.js|ts which does not import httpRouter", const fixtureDir = dirname + "/test_fixtures/js/project_with_https_without_router"; const logSpy = vi.spyOn(process.stderr, "write"); - await entryPoints(oneoffContext(), fixtureDir, false); + await entryPoints(oneoffContext(), fixtureDir); expect(logSpy).toHaveBeenCalledTimes(0); }); diff --git a/npm-packages/convex/src/bundler/index.ts b/npm-packages/convex/src/bundler/index.ts index 71fcb737..ebf398f2 100644 --- a/npm-packages/convex/src/bundler/index.ts +++ b/npm-packages/convex/src/bundler/index.ts @@ -5,7 +5,7 @@ import { parse as parseAST } from "@babel/parser"; import { Identifier, ImportSpecifier } from "@babel/types"; import * as Sentry from "@sentry/node"; import { Filesystem } from "./fs.js"; -import { Context, logWarning } from "./context.js"; +import { Context, logVerbose, logWarning } from "./context.js"; import { wasmPlugin } from "./wasm.js"; import { ExternalPackage, @@ -315,16 +315,9 @@ export async function doesImportConvexHttpRouter(source: string) { export async function entryPoints( ctx: Context, dir: string, - verbose: boolean, ): Promise { const entryPoints = []; - const log = (line: string) => { - if (verbose) { - console.log(line); - } - }; - for (const { isDir, path: fpath, depth } of walkDir(ctx.fs, dir)) { if (isDir) { continue; @@ -358,33 +351,39 @@ export async function entryPoints( } if (relPath.startsWith("_generated" + path.sep)) { - log(chalk.yellow(`Skipping ${fpath}`)); + logVerbose(ctx, chalk.yellow(`Skipping ${fpath}`)); } else if (base.startsWith(".")) { - log(chalk.yellow(`Skipping dotfile ${fpath}`)); + logVerbose(ctx, chalk.yellow(`Skipping dotfile ${fpath}`)); } else if (base.startsWith("#")) { - log(chalk.yellow(`Skipping likely emacs tempfile ${fpath}`)); + logVerbose(ctx, chalk.yellow(`Skipping likely emacs tempfile ${fpath}`)); } else if (base === "README.md") { - log(chalk.yellow(`Skipping ${fpath}`)); + logVerbose(ctx, chalk.yellow(`Skipping ${fpath}`)); } else if (base === "_generated.ts") { - log(chalk.yellow(`Skipping ${fpath}`)); + logVerbose(ctx, chalk.yellow(`Skipping ${fpath}`)); } else if (base === "schema.ts" || base === "schema.js") { - log(chalk.yellow(`Skipping ${fpath}`)); + logVerbose(ctx, chalk.yellow(`Skipping ${fpath}`)); } else if ((base.match(/\./g) || []).length > 1) { - log(chalk.yellow(`Skipping ${fpath} that contains multiple dots`)); + logVerbose( + ctx, + chalk.yellow(`Skipping ${fpath} that contains multiple dots`), + ); } else if (base === "tsconfig.json") { - log(chalk.yellow(`Skipping ${fpath}`)); + logVerbose(ctx, chalk.yellow(`Skipping ${fpath}`)); } else if (relPath.endsWith(".config.js")) { - log(chalk.yellow(`Skipping ${fpath}`)); + logVerbose(ctx, chalk.yellow(`Skipping ${fpath}`)); } else if (relPath.includes(" ")) { - log(chalk.yellow(`Skipping ${relPath} because it contains a space`)); + logVerbose( + ctx, + chalk.yellow(`Skipping ${relPath} because it contains a space`), + ); } else if (base.endsWith(".d.ts")) { - log(chalk.yellow(`Skipping ${fpath} declaration file`)); + logVerbose(ctx, chalk.yellow(`Skipping ${fpath} declaration file`)); } else if (base.endsWith(".json")) { - log(chalk.yellow(`Skipping ${fpath} json file`)); + logVerbose(ctx, chalk.yellow(`Skipping ${fpath} json file`)); } else if (base.endsWith(".jsonl")) { - log(chalk.yellow(`Skipping ${fpath} jsonl file`)); + logVerbose(ctx, chalk.yellow(`Skipping ${fpath} jsonl file`)); } else { - log(chalk.green(`Preparing ${fpath}`)); + logVerbose(ctx, chalk.green(`Preparing ${fpath}`)); entryPoints.push(fpath); } } @@ -400,7 +399,8 @@ export async function entryPoints( if (/^\s{0,100}(import|export)/m.test(contents)) { return true; } - log( + logVerbose( + ctx, chalk.yellow( `Skipping ${fpath} because it has no export or import to make it a valid TypeScript module`, ), @@ -413,14 +413,10 @@ export async function entryPoints( // A fallback regex in case we fail to parse the AST. export const useNodeDirectiveRegex = /^\s*("|')use node("|');?\s*$/; -function hasUseNodeDirective( - fs: Filesystem, - fpath: string, - verbose: boolean, -): boolean { +function hasUseNodeDirective(ctx: Context, fpath: string): boolean { // Do a quick check for the exact string. If it doesn't exist, don't // bother parsing. - const source = fs.readUtf8File(fpath); + const source = ctx.fs.readUtf8File(fpath); if (source.indexOf("use node") === -1) { return false; } @@ -451,12 +447,11 @@ function hasUseNodeDirective( } } - if (verbose) { - // Log that we failed to parse in verbose node if we need this for debugging. - console.warn( - `Failed to parse ${fpath}. Use node is set to ${lineMatches} based on regex. Parse error: ${error.toString()}.`, - ); - } + // Log that we failed to parse in verbose node if we need this for debugging. + logVerbose( + ctx, + `Failed to parse ${fpath}. Use node is set to ${lineMatches} based on regex. Parse error: ${error.toString()}.`, + ); return lineMatches; } @@ -473,11 +468,10 @@ async function determineEnvironment( ctx: Context, dir: string, fpath: string, - verbose: boolean, ): Promise { const relPath = path.relative(dir, fpath); - const useNodeDirectiveFound = hasUseNodeDirective(ctx.fs, fpath, verbose); + const useNodeDirectiveFound = hasUseNodeDirective(ctx, fpath); if (useNodeDirectiveFound) { if (mustBeIsolate(relPath)) { return await ctx.crash({ @@ -501,20 +495,11 @@ async function determineEnvironment( return "isolate"; } -export async function entryPointsByEnvironment( - ctx: Context, - dir: string, - verbose: boolean, -) { +export async function entryPointsByEnvironment(ctx: Context, dir: string) { const isolate = []; const node = []; - for (const entryPoint of await entryPoints(ctx, dir, verbose)) { - const environment = await determineEnvironment( - ctx, - dir, - entryPoint, - verbose, - ); + for (const entryPoint of await entryPoints(ctx, dir)) { + const environment = await determineEnvironment(ctx, dir, entryPoint); if (environment === "node") { node.push(entryPoint); } else { diff --git a/npm-packages/convex/src/cli/codegen_templates/component_api.ts b/npm-packages/convex/src/cli/codegen_templates/component_api.ts index accd41a1..55ac9e4d 100644 --- a/npm-packages/convex/src/cli/codegen_templates/component_api.ts +++ b/npm-packages/convex/src/cli/codegen_templates/component_api.ts @@ -77,7 +77,7 @@ export async function componentApiDTS( rootComponent, componentDirectory, ); - const absModulePaths = await entryPoints(ctx, componentDirectory.path, false); + const absModulePaths = await entryPoints(ctx, componentDirectory.path); const modulePaths = absModulePaths.map((p) => path.relative(componentDirectory.path, p), ); diff --git a/npm-packages/convex/src/cli/dashboard.ts b/npm-packages/convex/src/cli/dashboard.ts index 7cb3c810..0c62a4e5 100644 --- a/npm-packages/convex/src/cli/dashboard.ts +++ b/npm-packages/convex/src/cli/dashboard.ts @@ -1,7 +1,7 @@ import { Command } from "@commander-js/extra-typings"; import chalk from "chalk"; import open from "open"; -import { logMessage, oneoffContext } from "../bundler/context.js"; +import { logMessage, logOutput, oneoffContext } from "../bundler/context.js"; import { deploymentSelectionFromOptions, fetchDeploymentCredentialsProvisionProd, @@ -48,7 +48,7 @@ export const dashboard = new Command("dashboard") ); await open(loginUrl); } else { - console.log(loginUrl); + logOutput(ctx, loginUrl); } }); diff --git a/npm-packages/convex/src/cli/deployments.ts b/npm-packages/convex/src/cli/deployments.ts index 86902979..98c4935b 100644 --- a/npm-packages/convex/src/cli/deployments.ts +++ b/npm-packages/convex/src/cli/deployments.ts @@ -2,7 +2,12 @@ import { Command } from "@commander-js/extra-typings"; import { readProjectConfig } from "./lib/config.js"; import chalk from "chalk"; import { bigBrainAPI } from "./lib/utils/utils.js"; -import { logError, logMessage, oneoffContext } from "../bundler/context.js"; +import { + logError, + logMessage, + logOutput, + oneoffContext, +} from "../bundler/context.js"; type Deployment = { id: number; @@ -25,7 +30,7 @@ export const deployments = new Command("deployments") method: "GET", url, })) as Deployment[]; - console.log(deployments); + logOutput(ctx, deployments); if (deployments.length === 0) { logError(ctx, chalk.yellow(`No deployments exist for project`)); } diff --git a/npm-packages/convex/src/cli/dev.ts b/npm-packages/convex/src/cli/dev.ts index bcb47b04..2d6c53b0 100644 --- a/npm-packages/convex/src/cli/dev.ts +++ b/npm-packages/convex/src/cli/dev.ts @@ -4,6 +4,7 @@ import path from "path"; import { performance } from "perf_hooks"; import { OneoffCtx, + logError, logFinishedStep, logMessage, logVerbose, @@ -278,6 +279,8 @@ export async function watchAndPush( } // Fall through if we had a filesystem-based error. + // TODO(sarah): Replace this with `logError`. + // eslint-disable-next-line no-console console.assert( e.errorType === "invalid filesystem data" || e.errorType === "invalid filesystem or env vars" || @@ -526,8 +529,13 @@ function getFileSystemWatch( } } } else { - console.assert(result === "timeout"); // Let the check above `break` from the loop if we're past our deadlne. + if (result !== "timeout") { + logError( + ctx, + "Assertion failed: Unexpected result from watcher: " + result, + ); + } } } }, diff --git a/npm-packages/convex/src/cli/docs.ts b/npm-packages/convex/src/cli/docs.ts index 5a000463..2e62e89f 100644 --- a/npm-packages/convex/src/cli/docs.ts +++ b/npm-packages/convex/src/cli/docs.ts @@ -1,7 +1,7 @@ import { Command } from "@commander-js/extra-typings"; import chalk from "chalk"; import open from "open"; -import { oneoffContext } from "../bundler/context.js"; +import { Context, logMessage, oneoffContext } from "../bundler/context.js"; import { getTargetDeploymentName } from "./lib/deployment.js"; import { bigBrainFetch, deprecationCheckWarning } from "./lib/utils/utils.js"; @@ -19,21 +19,21 @@ export const docs = new Command("docs") const res = await fetch(getCookieUrl); deprecationCheckWarning(ctx, res); const { cookie } = await res.json(); - await openDocs(options.open, cookie); + await openDocs(ctx, options.open, cookie); } catch { - await openDocs(options.open); + await openDocs(ctx, options.open); } }); -async function openDocs(toOpen: boolean, cookie?: string) { +async function openDocs(ctx: Context, toOpen: boolean, cookie?: string) { let docsUrl = "https://docs.convex.dev"; if (cookie !== undefined) { docsUrl += "/?t=" + cookie; } if (toOpen) { await open(docsUrl); - console.log(chalk.green("Docs have launched! Check your browser.")); + logMessage(ctx, chalk.green("Docs have launched! Check your browser.")); } else { - console.log(chalk.green(`Find Convex docs here: ${docsUrl}`)); + logMessage(ctx, chalk.green(`Find Convex docs here: ${docsUrl}`)); } } diff --git a/npm-packages/convex/src/cli/index.ts b/npm-packages/convex/src/cli/index.ts index 129969c7..be975bdc 100644 --- a/npm-packages/convex/src/cli/index.ts +++ b/npm-packages/convex/src/cli/index.ts @@ -134,6 +134,8 @@ async function main() { } catch (e) { Sentry.captureException(e); process.exitCode = 1; + // This is too early to use `logError`, so just log directly. + // eslint-disable-next-line no-console console.error(chalk.red("Unexpected Error: " + e)); } finally { await Sentry.close(); diff --git a/npm-packages/convex/src/cli/lib/codegen.ts b/npm-packages/convex/src/cli/lib/codegen.ts index 4825aac0..28bce3c8 100644 --- a/npm-packages/convex/src/cli/lib/codegen.ts +++ b/npm-packages/convex/src/cli/lib/codegen.ts @@ -488,7 +488,7 @@ async function doApiCodegen( generateCommonJSApi: boolean, opts?: { dryRun?: boolean; debug?: boolean }, ) { - const absModulePaths = await entryPoints(ctx, functionsDir, false); + const absModulePaths = await entryPoints(ctx, functionsDir); const modulePaths = absModulePaths.map((p) => path.relative(functionsDir, p)); const apiContent = apiCodegen(modulePaths); diff --git a/npm-packages/convex/src/cli/lib/components/definition/bundle.ts b/npm-packages/convex/src/cli/lib/components/definition/bundle.ts index 80cbc0d1..fbad4d19 100644 --- a/npm-packages/convex/src/cli/lib/components/definition/bundle.ts +++ b/npm-packages/convex/src/cli/lib/components/definition/bundle.ts @@ -246,6 +246,7 @@ export async function componentGraph( }); } for (const warning of result.warnings) { + // eslint-disable-next-line no-console console.log(chalk.yellow(`esbuild warning: ${warning.text}`)); } return await findComponentDependencies(ctx, result.metafile); @@ -380,6 +381,7 @@ export async function bundleDefinitions( }); } for (const warning of result.warnings) { + // eslint-disable-next-line no-console console.log(chalk.yellow(`esbuild warning: ${warning.text}`)); } @@ -506,11 +508,7 @@ export async function bundleImplementations( schema = null; } - const entryPoints = await entryPointsByEnvironment( - ctx, - resolvedPath, - verbose, - ); + const entryPoints = await entryPointsByEnvironment(ctx, resolvedPath); const convexResult: { modules: Bundle[]; externalDependencies: Map; diff --git a/npm-packages/convex/src/cli/lib/config.ts b/npm-packages/convex/src/cli/lib/config.ts index 7b3cc56e..c6966bdf 100644 --- a/npm-packages/convex/src/cli/lib/config.ts +++ b/npm-packages/convex/src/cli/lib/config.ts @@ -328,7 +328,7 @@ export async function configFromProjectConfig( const baseDir = functionsDir(configPath, projectConfig); // We bundle functions entry points separately since they execute on different // platforms. - const entryPoints = await entryPointsByEnvironment(ctx, baseDir, verbose); + const entryPoints = await entryPointsByEnvironment(ctx, baseDir); // es-build prints errors to console which would clobber // our spinner. if (verbose) { diff --git a/npm-packages/convex/src/cli/lib/deploy2.ts b/npm-packages/convex/src/cli/lib/deploy2.ts index 7f0c0b90..c5854759 100644 --- a/npm-packages/convex/src/cli/lib/deploy2.ts +++ b/npm-packages/convex/src/cli/lib/deploy2.ts @@ -3,7 +3,7 @@ import { Context, logError, logFailure, - logMessage, + logVerbose, } from "../../bundler/context.js"; import { deploymentFetch, @@ -32,11 +32,7 @@ import zlib from "node:zlib"; const brotli = promisify(zlib.brotliCompress); -async function brotliCompress( - ctx: Context, - data: string, - opts?: { verbose?: boolean }, -): Promise { +async function brotliCompress(ctx: Context, data: string): Promise { const start = performance.now(); const result = await brotli(data, { params: { @@ -45,13 +41,11 @@ async function brotliCompress( }, }); const end = performance.now(); - if (opts?.verbose) { - const duration = end - start; - logMessage( - ctx, - `Compressed ${(data.length / 1024).toFixed(2)}KiB to ${(result.length / 1024).toFixed(2)}KiB (${((result.length / data.length) * 100).toFixed(2)}%) in ${duration.toFixed(2)}ms`, - ); - } + const duration = end - start; + logVerbose( + ctx, + `Compressed ${(data.length / 1024).toFixed(2)}KiB to ${(result.length / 1024).toFixed(2)}KiB (${((result.length / data.length) * 100).toFixed(2)}%) in ${duration.toFixed(2)}ms`, + ); return result; } @@ -62,14 +56,11 @@ export async function startPush( request: StartPushRequest, options: { url: string; - verbose?: boolean; }, ): Promise { - if (options.verbose) { - const custom = (_k: string | number, s: any) => - typeof s === "string" ? s.slice(0, 40) + (s.length > 40 ? "..." : "") : s; - console.log(JSON.stringify(request, custom, 2)); - } + const custom = (_k: string | number, s: any) => + typeof s === "string" ? s.slice(0, 40) + (s.length > 40 ? "..." : "") : s; + logVerbose(ctx, JSON.stringify(request, custom, 2)); const onError = (err: any) => { if (err.toString() === "TypeError: fetch failed") { changeSpinner( @@ -82,7 +73,7 @@ export async function startPush( changeSpinner(ctx, "Analyzing and deploying source code..."); try { const response = await fetch("/api/deploy2/start_push", { - body: await brotliCompress(ctx, JSON.stringify(request), options), + body: await brotliCompress(ctx, JSON.stringify(request)), method: "POST", headers: { "Content-Type": "application/json", @@ -248,7 +239,7 @@ export async function finishPush( }; try { const response = await fetch("/api/deploy2/finish_push", { - body: await brotliCompress(ctx, JSON.stringify(request), options), + body: await brotliCompress(ctx, JSON.stringify(request)), method: "POST", headers: { "Content-Type": "application/json", diff --git a/npm-packages/convex/src/cli/lib/login.ts b/npm-packages/convex/src/cli/lib/login.ts index fcf944b6..fc223618 100644 --- a/npm-packages/convex/src/cli/lib/login.ts +++ b/npm-packages/convex/src/cli/lib/login.ts @@ -19,6 +19,7 @@ import { logFailure, logFinishedStep, logMessage, + logOutput, showSpinner, } from "../../bundler/context.js"; import { Issuer } from "openid-client"; @@ -369,7 +370,7 @@ export async function performLogin( } if (dumpAccessToken) { - console.log(`${accessToken}`); + logOutput(ctx, `${accessToken}`); return await ctx.crash({ exitCode: 0, errorType: "fatal", diff --git a/npm-packages/convex/src/cli/lib/push.ts b/npm-packages/convex/src/cli/lib/push.ts index a59d5064..b5c81fe8 100644 --- a/npm-packages/convex/src/cli/lib/push.ts +++ b/npm-packages/convex/src/cli/lib/push.ts @@ -43,6 +43,9 @@ export async function runNonComponentsPush( const timeRunPushStarts = performance.now(); const origin = options.url; const verbose = options.verbose || options.dryRun; + if (verbose) { + process.env["CONVEX_VERBOSE"] = "1"; + } await ensureHasConvexDependency(ctx, "push"); if (!options.codegen) { diff --git a/npm-packages/convex/src/cli/update.ts b/npm-packages/convex/src/cli/update.ts index 34d9c65a..772bcbd6 100644 --- a/npm-packages/convex/src/cli/update.ts +++ b/npm-packages/convex/src/cli/update.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import { Command } from "@commander-js/extra-typings"; -import { oneoffContext } from "../bundler/context.js"; +import { logMessage, oneoffContext } from "../bundler/context.js"; import { loadPackageJson } from "./lib/utils/utils.js"; export const update = new Command("update") @@ -16,7 +16,8 @@ export const update = new Command("update") updateInstructions += `npm uninstall ${pkg}\n`; } - console.log( + logMessage( + ctx, chalk.green( `To view the Convex changelog, go to https://news.convex.dev/tag/releases/\nWhen you are ready to upgrade, run the following commands:\n${updateInstructions}`, ), diff --git a/npm-packages/convex/src/react/client.ts b/npm-packages/convex/src/react/client.ts index 737aef7b..79f9149d 100644 --- a/npm-packages/convex/src/react/client.ts +++ b/npm-packages/convex/src/react/client.ts @@ -22,6 +22,7 @@ import { makeFunctionReference, } from "../server/api.js"; import { EmptyObject } from "../server/registration.js"; +import { instantiateDefaultLogger, Logger } from "../browser/logging.js"; if (typeof React === "undefined") { throw new Error("Required dependency 'react' not found"); @@ -228,6 +229,7 @@ export class ConvexReactClient { private listeners: Map void>>; private options: ConvexReactClientOptions; private closed = false; + private _logger: Logger; private adminAuth?: string; private fakeUserIdentity?: UserIdentityAttributes; @@ -257,7 +259,10 @@ export class ConvexReactClient { } this.address = address; this.listeners = new Map(); - this.options = { ...options }; + this._logger = + options?.logger ?? + instantiateDefaultLogger({ verbose: options?.verbose ?? false }); + this.options = { ...options, logger: this._logger }; } /** @@ -487,6 +492,15 @@ export class ConvexReactClient { return this.sync.connectionState(); } + /** + * Get the logger for this client. + * + * @returns The {@link Logger} for this client. + */ + get logger(): Logger { + return this._logger; + } + /** * Close any network handles associated with this client and stop all subscriptions. * diff --git a/npm-packages/convex/src/react/use_paginated_query.ts b/npm-packages/convex/src/react/use_paginated_query.ts index ed883d00..5493e91b 100644 --- a/npm-packages/convex/src/react/use_paginated_query.ts +++ b/npm-packages/convex/src/react/use_paginated_query.ts @@ -15,6 +15,7 @@ import { getFunctionName, } from "../server/api.js"; import { BetterOmit, Expand } from "../type_utils.js"; +import { useConvex } from "./client.js"; /** * A {@link server.FunctionReference} that is usable with {@link usePaginatedQuery}. @@ -227,6 +228,8 @@ export function usePaginatedQuery( currState = createInitialState(); setState(currState); } + const convexClient = useConvex(); + const logger = convexClient.logger; const resultsObject = useQueries(currState.queries); @@ -252,7 +255,7 @@ export function usePaginatedQuery( // In all cases, we want to restart pagination to throw away all our // existing cursors. - console.warn( + logger.warn( "usePaginatedQuery hit error, resetting pagination state: " + currResult.message, ); @@ -302,6 +305,7 @@ export function usePaginatedQuery( currState.ongoingSplits, options.initialNumItems, createInitialState, + logger, ]); const statusObject = useMemo(() => {