diff --git a/.cursor/rules/frontend-core.mdc b/.cursor/rules/frontend-core.mdc index 3bb3de5f7..f1c9a9dd5 100644 --- a/.cursor/rules/frontend-core.mdc +++ b/.cursor/rules/frontend-core.mdc @@ -153,7 +153,7 @@ import type { ProjectFragment } from '~/lib/common/generated/gql/graphql' - **Structured logging** with Pino for production - **useLogger()** composable for standard logging -- **useStrictLogger()** when you need guaranteed real Pino logger +- **useSafeLogger()** when you need a logger potentially outside of useNuxtApp() scope - **devLog()** or **useDevLogger()** for development-only logging - **Never use console.log** - use logging composables instead - **Development logging** is automatically skipped in production diff --git a/packages/frontend-2/composables/logging.ts b/packages/frontend-2/composables/logging.ts index 795c37104..a3f407b87 100644 --- a/packages/frontend-2/composables/logging.ts +++ b/packages/frontend-2/composables/logging.ts @@ -9,71 +9,95 @@ export const useLogger = () => { } /** - * Use when you need to be sure that the real structured pino logger is available - * (it isn't in some early startup contexts like apollo link setup) + * There are scopes where useNuxtApp() is available and we have to fall back to a different logger. This composable + * can be invoked everywhere in all scopes (not in `server` code tho!) to get the best kind of logger available */ -export const useStrictLogger = async ( - options?: Partial<{ dontNotifyFallback: boolean }> +export const useSafeLogger = ( + options?: Partial<{ + /** + * Prevent reporting back that we used a logger fallback. + * + * Default: false + */ + preventFallbackReporting: boolean + }> ) => { - const { dontNotifyFallback } = options || {} - + let fallbackReported = false let nuxtApp: Optional = undefined - try { - nuxtApp = useNuxtApp() - } catch { - // suppress 'nuxt is not available' + const fallbackSyncLogger = buildFakePinoLogger() + let fallbackFullLogger: Optional> = undefined + + const availableLogger = () => + nuxtApp?.$logger || fallbackFullLogger || fallbackSyncLogger + + const tryResolvingRealLogger = () => { + // Try nuxt app + try { + nuxtApp = useNuxtApp() + } catch { + // suppress 'nuxt is not available' + } } - if (nuxtApp?.$logger) return nuxtApp?.$logger + const tryResolvingLogger = async () => { + try { + tryResolvingRealLogger() + if (nuxtApp?.$logger) return - // Nuxt app not found in this scope - const err = new Error( - 'Nuxt app for logger not found! Initializing fallback structured logger...' - ) - - let logger: ReturnType - if (import.meta.server) { - const { buildLogger } = await import('~/server/lib/core/helpers/observability') - logger = buildLogger('info', import.meta.dev ? true : false) // no runtime config, so falling back to default settings - } else { - logger = buildFakePinoLogger() + if (import.meta.server && !fallbackFullLogger) { + const { buildLogger } = await import('~/server/lib/core/helpers/observability') + fallbackFullLogger = buildLogger('info', import.meta.dev ? true : false) // no runtime config, so falling back to default settings + } + } catch (err) { + availableLogger().error(err) + } } - if (!dontNotifyFallback) logger.error(err) + const logger = ( + loggerOptions?: Partial<{ + /** + * Prevent reporting back that we used a logger fallback. + * + * Default: false + */ + preventFallbackReporting: boolean + }> + ) => { + const preventFallbackReporting = + loggerOptions?.preventFallbackReporting || options?.preventFallbackReporting - return logger -} + void tryResolvingLogger() + if (nuxtApp?.$logger) return nuxtApp.$logger -/** - * Use when you need to be sure that the real structured pino logger is available - * (it isn't in some early startup contexts like apollo link setup) - * - * The async version is better in that it will build a real pino logger, but in sync contexts - * you can use this one that will at least fallback to console.log/warn/error - */ -export const useStrictLoggerSync = ( - options?: Partial<{ dontNotifyFallback: boolean }> -) => { - const { dontNotifyFallback } = options || {} + const err = new Error('Nuxt app for logger not found! Returning fallback logger...') + let logger: ReturnType + if (fallbackFullLogger) { + logger = fallbackFullLogger + } else { + logger = fallbackSyncLogger + } - let nuxtApp: Optional = undefined - try { - nuxtApp = useNuxtApp() - } catch { - // suppress 'nuxt is not available' + // we only wanna report this once + if (!preventFallbackReporting && !fallbackReported) { + logger.error(err) + fallbackReported = true + } + + return logger } - if (nuxtApp?.$logger) return nuxtApp?.$logger + void tryResolvingLogger() - // Nuxt app not found in this scope - const err = new Error( - 'Nuxt app for logger not found! Initializing fallback structured logger...' - ) - const logger = buildFakePinoLogger() - - if (!dontNotifyFallback) logger.error(err) - - return logger + return { + /** + * Get the best available logger instance + */ + logger, + /** + * If you're in a scope where you can invoke async code, invoke this to try to load the most appropriate logger + */ + loadBestLogger: tryResolvingLogger + } } /** @@ -83,7 +107,11 @@ export const useStrictLoggerSync = ( export const useDevLogger = () => { if (!import.meta.dev) return noop - const logger = useLogger() - const debug = logger.debug.bind(logger) - return debug as (...args: unknown[]) => void + const { logger } = useSafeLogger() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (...args: any[]) => { + const actualLogger = logger() + const debug = actualLogger.debug.bind(actualLogger) + return debug(args[0], ...args.slice(1)) //ts appeasement + } } diff --git a/packages/frontend-2/lib/auth/composables/auth.ts b/packages/frontend-2/lib/auth/composables/auth.ts index 4633a96a4..bc695b4ad 100644 --- a/packages/frontend-2/lib/auth/composables/auth.ts +++ b/packages/frontend-2/lib/auth/composables/auth.ts @@ -140,7 +140,7 @@ const useResetAuthState = ( const resolveDistinctId = useResolveUserDistinctId() const { cbs } = useOnAuthStateChangeState() const authToken = useAuthCookie() - const logger = useStrictLoggerSync() + const { logger } = useSafeLogger() return async ( resetOptions?: Partial<{ @@ -174,7 +174,7 @@ const useResetAuthState = ( // also depend on active user (e.g. Workspace.seatType)) resetPromise = (async () => { if (import.meta.server) { - logger?.error('attempting to resetStore from SSR') + logger().error('attempting to resetStore from SSR') } else { await client.resetStore() } @@ -229,7 +229,7 @@ export const useAuthManager = ( const getMixpanel = useDeferredMixpanel() const postAuthRedirect = usePostAuthRedirect() const { markLoggedOut } = useJustLoggedOutTracking() - const logger = useStrictLoggerSync() + const { logger } = useSafeLogger() /** * Invite token, if any @@ -379,7 +379,7 @@ export const useAuthManager = ( title: 'Authentication failed', description: err.message }) - logger.error({ err }, 'Failed to finalize login with access code') + logger().error({ err }, 'Failed to finalize login with access code') } } }, diff --git a/packages/frontend-2/lib/common/composables/async.ts b/packages/frontend-2/lib/common/composables/async.ts index f37f12e39..e5d09f3b9 100644 --- a/packages/frontend-2/lib/common/composables/async.ts +++ b/packages/frontend-2/lib/common/composables/async.ts @@ -13,7 +13,7 @@ export type { AsyncWritableComputedOptions, AsyncWritableComputedRef } * @param params */ export const writableAsyncComputed: typeof originalWritableAsyncComputed = (params) => { - const logger = useStrictLoggerSync() + const { logger } = useSafeLogger() return originalWritableAsyncComputed({ ...params, debugging: params.debugging?.log @@ -21,7 +21,7 @@ export const writableAsyncComputed: typeof originalWritableAsyncComputed = (para ...params.debugging, log: { ...params.debugging.log, - logger: logger.debug + logger: logger().debug } } : undefined @@ -34,7 +34,7 @@ export const writableAsyncComputed: typeof originalWritableAsyncComputed = (para */ export const watchAsync = ((...args: Parameters) => { const [source, cb, options] = args - const logger = useStrictLoggerSync() + const { logger } = useSafeLogger() const watches = shallowRef>>([]) @@ -47,7 +47,7 @@ export const watchAsync = ((...args: Parameters) => { const handlerPromise = Promise.allSettled(watches.value).finally(() => Promise.resolve(cb(newVal, oldVal, onCleanup)) .catch((e) => { - logger.error(e, 'Error occurred in watchAsync callback') + logger().error(e, 'Error occurred in watchAsync callback') throw e }) .finally(() => { diff --git a/packages/frontend-2/lib/common/helpers/debugging.ts b/packages/frontend-2/lib/common/helpers/debugging.ts index 3ae739421..b7bb35f01 100644 --- a/packages/frontend-2/lib/common/helpers/debugging.ts +++ b/packages/frontend-2/lib/common/helpers/debugging.ts @@ -11,19 +11,19 @@ export function wrapRefWithTracking>( ): R { const { writesOnly, readsOnly } = options || {} const getTrace = () => (new Error('Trace:').stack || '').substring(7) - const logger = useStrictLoggerSync() + const { logger } = useSafeLogger() return computed({ get: () => { if (!writesOnly) { - logger.debug(`debugging: '${name}' read`, ref.value, getTrace()) + logger().debug(`debugging: '${name}' read`, ref.value, getTrace()) } return ref.value }, set: (newVal) => { if (!readsOnly) { - logger.debug(`debugging: '${name}' written to`, newVal, getTrace()) + logger().debug(`debugging: '${name}' written to`, newVal, getTrace()) } ref.value = newVal diff --git a/packages/frontend-2/lib/common/helpers/graphql.ts b/packages/frontend-2/lib/common/helpers/graphql.ts index 4c99410fa..856538398 100644 --- a/packages/frontend-2/lib/common/helpers/graphql.ts +++ b/packages/frontend-2/lib/common/helpers/graphql.ts @@ -195,7 +195,7 @@ export function updateCacheByFilter( ): boolean { const { fragment, query } = filter const { ignoreCacheErrors = true, overwrite = true } = options - const logger = useStrictLoggerSync() + const { logger } = useSafeLogger() if (!fragment && !query) { throw new Error( @@ -242,7 +242,7 @@ export function updateCacheByFilter( } if (ignoreCacheErrors) { - logger.warn('Failed Apollo cache update:', e) + logger().warn('Failed Apollo cache update:', e) return false } throw e @@ -382,13 +382,13 @@ export function modifyObjectFields< ) { const { fieldNameWhitelist, debug = false } = options || {} - const logger = useStrictLoggerSync() + const { logger } = useSafeLogger() const invocationId = nanoid() - const log = (...args: Parameters) => { + const log = (...args: unknown[]) => { if (!debug) return const [message, ...rest] = args - logger.debug(`[${invocationId}] ${message}`, ...rest) + logger().debug(`[${invocationId}] ${message}`, ...rest) } log( diff --git a/packages/frontend-2/lib/common/helpers/route.ts b/packages/frontend-2/lib/common/helpers/route.ts index d345e7148..c62402b55 100644 --- a/packages/frontend-2/lib/common/helpers/route.ts +++ b/packages/frontend-2/lib/common/helpers/route.ts @@ -203,7 +203,7 @@ export const doesRouteFitTarget = (fullPathA: string, fullPathB: string) => { urlA = new URL(fullPathA, fakeOrigin) urlB = new URL(fullPathB, fakeOrigin) } catch (e) { - useStrictLoggerSync().warn('Failed to parse URLs', e) + useSafeLogger().logger().warn('Failed to parse URLs', e) return false } diff --git a/packages/frontend-2/lib/viewer/composables/setup/embed.ts b/packages/frontend-2/lib/viewer/composables/setup/embed.ts index 386d8c1fc..0e6a4ee0e 100644 --- a/packages/frontend-2/lib/viewer/composables/setup/embed.ts +++ b/packages/frontend-2/lib/viewer/composables/setup/embed.ts @@ -37,7 +37,7 @@ export function isEmbedOptions(obj: unknown): obj is EmbedOptions { } export function deserializeEmbedOptions(embedString: string | null): EmbedOptions { - const logger = useStrictLoggerSync() + const { logger } = useSafeLogger() if (!embedString) { return { isEnabled: false } } @@ -47,9 +47,9 @@ export function deserializeEmbedOptions(embedString: string | null): EmbedOption if (isEmbedOptions(parsed)) { return { ...parsed, isEnabled: true } } - logger.error('Parsed object is not of type EmbedOptions') + logger().error('Parsed object is not of type EmbedOptions') } catch (error) { - logger.error(error) + logger().error(error) } return { isEnabled: false }