refactor: Centralize logging and Sentry error reporting#871
refactor: Centralize logging and Sentry error reporting#871askov wants to merge 17 commits intosolana-foundation:masterfrom
Conversation
|
@askov is attempting to deploy a commit to the Solana Foundation Team on Vercel. A member of the Team first needs to authorize it. |
app/shared/lib/logger.ts
Outdated
| isLoggable(LOG_LEVEL.PANIC) && console.error(...formatArgs(message, context)); | ||
| } | ||
| error(message: string, context?: Record<string, unknown>) { | ||
| isLoggable(LOG_LEVEL.ERROR) && console.error(...formatArgs(message, context)); |
There was a problem hiding this comment.
Let's preserve the original logic about using the source error and fall back to a generic error, like here: https://github.com/solana-foundation/explorer/blob/master/app/utils/logger.ts#L44-L48
The intent was to save the original error without modifications. Having "Unrecognised error" means that something is wrong and requires attention.
There was a problem hiding this comment.
That also means that we have to wrap strings passed to Logger.error with error instances
There was a problem hiding this comment.
I'm not sure if I fully got your idea. I updated it to log 'unexpected' error types and to only send error instances to sentry. But converting an unexpected unknown to an error and console.error it with a default message seems unnecessary, doesn't it? You already have a debug log associated with it.
There was a problem hiding this comment.
I mean that Logger.error should allow passing an' Error' instance as its first argument. That seems reasonable.
There was a problem hiding this comment.
Oh, now I see what you mean.
Could you elaborate on this?
I don’t see any strong reasons for allowing the Error argument to come first.
Well, the only advantage I can see is that it makes catch blocks more ergonomic.
Logger.error(err)
// is shorter than
Logger.error(‘[idl:form] failed to load deps’, { error: err })However, the cons are:
- It breaks the uniform signature, making the Error the odd one out
- The quality of sentry messages degrades. If someone passes an Error directly and captureMessage is triggered, the sentry issue title becomes err.message, which is often generic (like "Network request failed" or “Bigint conversion failed”) instead of a stable, searchable string like [tx:fetch] failed to load transaction.
- Overloads are typically used for functions that can be called in fundamentally different ways for a reason. But why apply this to logs? I mean, it only increases developer friction and leads to unclear logs, such as Logger.error(error, { sentry: true }), where the error is often some third-party issue that the developer may not understand well.
The uniform (message, context) signature is a deliberate trade-off that prioritizes stable, searchable log messages and clean Sentry grouping over convenience at the call site. The "boilerplate" of Logger.error('[module] descriptive message', { error: err }) is actually a feature—it compels callers to describe what failed, rather than simply forwarding an opaque error.
There was a problem hiding this comment.
I see your point.
But I think not using Errors and allowing strings or objects to describe errors discourages their use in general, which is not good and prevents building structures that handle various errors separately.
captureMessage is triggered
If we write code and specifically pass e.message into the capture, we did this intentionally; I do not mean we have to pass it as is to make it shorter, but we can definitely do that if needed, for ex.: .catch(Logger.error).
What stops us from wrapping the original error with a fine-grained error?
Logger.error(new NamespacedError('[idl:form] failed to load deps'), { cause: err })
// OR
Logger.error(new IdlFormError('Failed to load deps', { cause: err }))I think the last line is almost identical to Logger.error(‘[idl:form] failed to load deps’, { error: err }), but the variant with an Error instance has more benefits in the long run than the variant with a string.
Speaking of the uniform signature. That is definitely a good thing, but we do not use captureMessage on par with Logger.error; it is inside the implementation, so we can extract the message from the error. Having a proper error-handling structure outweighs a uniform signature. I do not think we are aiming to allow using Logger.error and Sentry.captureMessage interchangeably.
Using overloads was just a proposal to describe the possibility of using Errors and all other types that fall into the unknown category. We can do unions or anything else.
To summarize:
- I do not mean we have to have the shortest form to log the error
- Error is designed to handle errors, and having fine-grained/namespaced errors is possible with it
There was a problem hiding this comment.
Okay, I agree with you that it's flexible and logical. I've updated the code.
However, the concern about this relaxed usage still remains. When you have an 'unknown' error parameter, it always (I'm intentionally exaggerating here, but you get what I mean - this is an open-source project 😄) leads to people simply using Logger.error(err) without actually creating any specialized error instances or causes.
There was a problem hiding this comment.
We could write an ESLint rule that would warn from using it that way. LGTM
…or non-Error values
…th additional context
3abff86 to
38ac7be
Compare
app/api/metadata/proxy/route.ts
Outdated
| const isPrivate = await checkURLForPrivateIP(parsedUrl); | ||
| if (isPrivate) { | ||
| Logger.error(new Error('Private IP detected'), parsedUrl.hostname); | ||
| Logger.error('[api:metadata-proxy] Private IP detected', { hostname: parsedUrl.hostname }); |
There was a problem hiding this comment.
the original error message will be lost here
resolveError() will convert it to new Error('Unrecognized error')
There was a problem hiding this comment.
Yeh, I missed a few of these during the latest refactoring commit (changed the signature to less strict). Fixed all the cases like this
| if (!response.ok) { | ||
| if (response.status === 429) { | ||
| Sentry.captureMessage('Coingecko API rate limit exceeded', { level: 'warning' }); | ||
| Logger.error('[api:coingecko] Rate limit exceeded', { sentry: true }); |
There was a problem hiding this comment.
we could also lose the error message
maybe Logger.warn would be fit for this place
There was a problem hiding this comment.
Replaced with warn (all the 429 in routes)
app/error.tsx
Outdated
|
|
||
| return ( | ||
| <div className="container mt-4"> | ||
| <ErrorCard text={error.message} retry={reset} /> |
There was a problem hiding this comment.
raw error message could be displayed to the user
There was a problem hiding this comment.
Updated to the generic message
| @@ -0,0 +1,18 @@ | |||
| 'use client'; | |||
There was a problem hiding this comment.
do we need tests for it or app/global-error.tsx?
Description
app/utils/logger(static class, default export) withapp/shared/lib/logger(singleton instance, named export)@sentry/nextjsimports replaced byLogger.panic()/Logger.warn({ sentry: true })or re-exports fromapp/shared/lib/sentry/. ESLintno-restricted-importsrule enforces this boundaryapp/error.tsxandapp/global-error.tsxcatch route-level and root-level errors, replacing per-component ErrorBoundary wrappersno-console: errorglobally to prevent regression@sentry/coreand@sentry/nextjsto10.39.0, migrate build config towebpack.treeshake.removeDebugLoggingtest-setup.specs.tsreplaces ~8 per-file mock blocksBreaking changes
NEXT_PUBLIC_ENABLE_CATCH_EXCEPTIONSenv var removed — Sentry capture is now always-on when DSN is configuredSENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNINGenv var removed@utils/logger→@/app/shared/lib/logger,SentryErrorBoundaryandwithTraceDatamoved to@/app/shared/lib/sentryType of change
Screenshots
Testing
Related Issues
Closes HOO-246
Checklist
build:infoscript to update build information