Skip to content

Commit 7834e64

Browse files
askovrogaldh
andauthored
refactor: Centralize logging and Sentry error reporting (#871)
## Description - **Centralized Logger**: Replace `app/utils/logger` (static class, default export) with `app/shared/lib/logger` (singleton instance, named export) - **Sentry consolidation**: All direct `@sentry/nextjs` imports replaced by `Logger.panic()`/`Logger.warn({ sentry: true })` or re-exports from `app/shared/lib/sentry/`. ESLint `no-restricted-imports` rule enforces this boundary - **Global error boundaries**: New `app/error.tsx` and `app/global-error.tsx` catch route-level and root-level errors, replacing per-component ErrorBoundary wrappers - **ESLint enforcement**: `no-console: error` globally to prevent regression - **Sentry SDK upgrade**: Pin `@sentry/core` and `@sentry/nextjs` to `10.39.0`, migrate build config to `webpack.treeshake.removeDebugLogging` - **Test cleanup**: Global Logger mock in `test-setup.specs.ts` replaces ~8 per-file mock blocks ### Breaking changes - `NEXT_PUBLIC_ENABLE_CATCH_EXCEPTIONS` env var removed — Sentry capture is now always-on when DSN is configured - `SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING` env var removed - Import paths changed: `@utils/logger` → `@/app/shared/lib/logger`, `SentryErrorBoundary` and `withTraceData` moved to `@/app/shared/lib/sentry` ## Type of change <!-- Check the appropriate options that apply to this PR --> - [x] Other (please describe): Enable sentry for all pages, logger refactoring ## Screenshots <!-- For UI changes, especially protocol screens, include screenshots showing the changes --> <!-- This is REQUIRED for protocol integration PRs --> ## Testing <!-- Describe how you tested your changes --> <!-- For protocol integrations, explain how you verified the protocol data is correctly displayed --> ## Related Issues Closes [HOO-246](https://linear.app/solana-fndn/issue/HOO-246/enable-sentry-for-all-the-pages) ## Checklist <!-- Verify that you have completed the following before requesting review --> - [x] My code follows the project's style guidelines - [x] I have added tests that prove my fix/feature works - [x] All tests pass locally and in CI - [x] I have updated documentation as needed - [x] I have run `build:info` script to update build information - [x] CI/CD checks pass --------- Co-authored-by: Sergo <rogaldh@radsh.red>
1 parent 21e11e8 commit 7834e64

File tree

129 files changed

+1507
-1042
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

129 files changed

+1507
-1042
lines changed

.env.example

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
NEXT_PUBLIC_MAINNET_RPC_URL=
44
NEXT_PUBLIC_DEVNET_RPC_URL=
55
NEXT_PUBLIC_TESTNET_RPC_URL=
6-
## Configuration for "Logger". Set max level of logs to see (0..5)
7-
NEXT_LOG_LEVEL=0
6+
## Configuration for "Logger". Set max level of logs to see (0..4)
7+
## 0=PANIC, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG
8+
## All logging is suppressed when this variable is not set.
9+
NEXT_LOG_LEVEL=4
810
## Configuration for IBRL Block Explorer
911
NEXT_PUBLIC_IBRL_EXPLORER_URL=
1012
## Configuration for "metadata" service. Set "**_ENABLED=true" to enable
@@ -52,11 +54,12 @@ RUGCHECK_API_KEY=
5254
COINGECKO_API_KEY=
5355
### Configuration for Bluprynt API
5456
BLUPRYNT_CREDENTIAL_AUTHORITY=
55-
## Configuration for Sentry
56-
### Flag whether to catch exceptions with the boundary on the client or not
57-
NEXT_PUBLIC_ENABLE_CATCH_EXCEPTIONS=1
58-
### Flag that allows to not collect errors globally (allows to track for errors that we need at the moment
59-
SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1
57+
## Configuration for Sentry feature enabled
58+
### Sentry is disabled by default. To enable error reporting, set SENTRY_DSN to
59+
### your project's DSN (found in Sentry under Settings > Projects > Client Keys).
60+
### Without a DSN all Sentry calls are silent no-ops.
61+
### SENTRY_ORG, SENTRY_PRJ, and SENTRY_AUTH_TOKEN are only needed for source-map
62+
### uploads during production builds.
6063
SENTRY_ORG=
6164
SENTRY_DSN=
6265
SENTRY_PRJ=

.eslintrc.json

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"message": "RegExps are not recommended. If you sure regexp is needed - please use eslint-disable-next no-restricted-syntax -- %comment% to explain why"
2323
}
2424
],
25-
"@eslint-community/eslint-comments/no-unlimited-disable": "error"
25+
"@eslint-community/eslint-comments/no-unlimited-disable": "error",
26+
"no-console": "error"
2627
},
2728
"overrides": [
2829
// Only uses Testing Library lint rules in test files
@@ -36,6 +37,31 @@
3637
"rules": {
3738
"@eslint-community/eslint-comments/no-unlimited-disable": "off"
3839
}
40+
},
41+
// Allow console.* in the Logger implementation, scripts, and pnpm hooks
42+
{
43+
"files": ["app/shared/lib/logger.ts", "scripts/**", ".pnpmfile.cjs"],
44+
"rules": {
45+
"no-console": "off"
46+
}
47+
},
48+
// Forbid direct @sentry/nextjs imports in app code — use @/app/shared/lib/sentry instead
49+
{
50+
"files": ["app/**/*.[jt]s?(x)"],
51+
"excludedFiles": ["app/shared/lib/sentry/**", "app/shared/lib/logger.ts"],
52+
"rules": {
53+
"no-restricted-imports": [
54+
"error",
55+
{
56+
"paths": [
57+
{
58+
"name": "@sentry/nextjs",
59+
"message": "Import from '@/app/shared/lib/sentry' instead. For logging, use the Logger from '@/app/shared/lib/logger'."
60+
}
61+
]
62+
}
63+
]
64+
}
3965
}
4066
]
4167
}

.github/workflows/ci.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ env:
1111
PNPM_CACHE_FOLDER: ~/.pnpm-store
1212
PW_CACHE_AFFIX: pw
1313
PW_CACHE_FOLDER: ~/.cache/ms-playwright
14-
SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING: 1
1514
SENTRY_TELEMETRY_DISABLE: true
1615
STORYBOOK_DISABLE_TELEMETRY: 1
1716

__tests__/middleware.spec.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ vi.mock('botid/server', () => ({
88
checkBotId: vi.fn(),
99
}));
1010

11-
import Logger from '@utils/logger';
11+
import { Logger } from '@/app/shared/lib/logger';
1212
import { checkBotId } from 'botid/server';
1313

1414
import { middleware } from '../middleware';
@@ -78,7 +78,8 @@ describe('middleware', () => {
7878
expect(response.status).toBe(200);
7979
expect(checkBotId).not.toHaveBeenCalled();
8080
expect(loggerInfoSpy).toHaveBeenCalledWith(
81-
expect.stringContaining('[middleware] No x-is-human header')
81+
'[middleware] No x-is-human header, allowing',
82+
expect.objectContaining({ pathname: '/api/test' })
8283
);
8384
});
8485
});
@@ -98,10 +99,13 @@ describe('middleware', () => {
9899
expect(response.status).toBe(200);
99100
expect(checkBotId).toHaveBeenCalled();
100101
expect(loggerInfoSpy).toHaveBeenCalledWith(
101-
expect.stringContaining('[middleware] BotId verification'),
102+
'[middleware] BotId verification',
102103
expect.objectContaining({ isHuman: true })
103104
);
104-
expect(loggerInfoSpy).toHaveBeenCalledWith(expect.stringContaining('[middleware] Human verified'));
105+
expect(loggerInfoSpy).toHaveBeenCalledWith(
106+
'[middleware] Human verified',
107+
expect.objectContaining({ pathname: '/api/test' })
108+
);
105109
});
106110

107111
it('should allow bot requests when challenge mode is disabled and log warning', async () => {
@@ -116,7 +120,10 @@ describe('middleware', () => {
116120
const response = await middleware(request);
117121

118122
expect(response.status).toBe(200);
119-
expect(loggerWarnSpy).toHaveBeenCalledWith(expect.stringContaining('[middleware] Bot detected'));
123+
expect(loggerWarnSpy).toHaveBeenCalledWith(
124+
'[middleware] Bot detected',
125+
expect.objectContaining({ pathname: '/api/test' })
126+
);
120127
});
121128
});
122129

@@ -139,11 +146,13 @@ describe('middleware', () => {
139146
expect(response.status).toBe(401);
140147
const body = await response.json();
141148
expect(body).toEqual({ error: 'Access denied: request identified as automated bot' });
142-
expect(loggerWarnSpy).toHaveBeenCalledWith(expect.stringContaining('[middleware] Bot detected'));
149+
expect(loggerWarnSpy).toHaveBeenCalledWith(
150+
'[middleware] Bot detected',
151+
expect.objectContaining({ pathname: '/api/test' })
152+
);
143153
expect(loggerErrorSpy).toHaveBeenCalledWith(
144-
expect.objectContaining({
145-
message: expect.stringContaining('[middleware] Challenge mode enabled, blocking'),
146-
})
154+
new Error('[middleware] Challenge mode enabled, blocking'),
155+
expect.objectContaining({ pathname: '/api/test' })
147156
);
148157
});
149158

@@ -160,9 +169,8 @@ describe('middleware', () => {
160169

161170
expect(response.status).toBe(401);
162171
expect(loggerErrorSpy).toHaveBeenCalledWith(
163-
expect.objectContaining({
164-
message: expect.stringContaining('[middleware] Challenge mode enabled, blocking'),
165-
})
172+
new Error('[middleware] Challenge mode enabled, blocking'),
173+
expect.objectContaining({ pathname: '/api/test' })
166174
);
167175
});
168176

@@ -178,7 +186,10 @@ describe('middleware', () => {
178186
const response = await middleware(request);
179187

180188
expect(response.status).toBe(200);
181-
expect(loggerInfoSpy).toHaveBeenCalledWith(expect.stringContaining('[middleware] Human verified'));
189+
expect(loggerInfoSpy).toHaveBeenCalledWith(
190+
'[middleware] Human verified',
191+
expect.objectContaining({ pathname: '/api/test' })
192+
);
182193
});
183194
});
184195
});
@@ -199,7 +210,10 @@ describe('middleware', () => {
199210
const response = await middleware(request);
200211

201212
expect(response.status).toBe(200);
202-
expect(loggerWarnSpy).toHaveBeenCalledWith(expect.stringContaining('[middleware] Bot detected'));
213+
expect(loggerWarnSpy).toHaveBeenCalledWith(
214+
'[middleware] Bot detected',
215+
expect.objectContaining({ pathname: '/api/test' })
216+
);
203217
});
204218

205219
it('should block request when both simulate bot mode and challenge mode are enabled', async () => {
@@ -219,9 +233,8 @@ describe('middleware', () => {
219233

220234
expect(response.status).toBe(401);
221235
expect(loggerErrorSpy).toHaveBeenCalledWith(
222-
expect.objectContaining({
223-
message: expect.stringContaining('[middleware] Challenge mode enabled, blocking'),
224-
})
236+
new Error('[middleware] Challenge mode enabled, blocking'),
237+
expect.objectContaining({ pathname: '/api/test' })
225238
);
226239
});
227240
});
Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import { captureException } from '@sentry/nextjs';
21
import { ComponentType } from 'react';
3-
import { ErrorBoundary } from 'react-error-boundary';
4-
5-
import { ErrorCard } from '@/app/components/common/ErrorCard';
6-
7-
const isSentryEnabled = process.env.NEXT_PUBLIC_ENABLE_CATCH_EXCEPTIONS === '1';
82

93
export function PageRenderer({
104
address,
@@ -13,16 +7,5 @@ export function PageRenderer({
137
address: string;
148
renderComponent: ComponentType<{ address: string }>;
159
}) {
16-
return (
17-
<ErrorBoundary
18-
onError={(error: Error) => {
19-
if (isSentryEnabled) {
20-
captureException(error);
21-
}
22-
}}
23-
fallbackRender={({ error }) => <ErrorCard text={`Failed to load: ${error.message}`} />}
24-
>
25-
<RenderComponent address={address} />
26-
</ErrorBoundary>
27-
);
10+
return <RenderComponent address={address} />;
2811
}

app/address/[address]/idl/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
22
import { Metadata } from 'next/types';
33

4-
import { withSentryTraceData } from '@/app/utils/with-sentry-trace-data';
4+
import { withTraceData } from '@/app/shared/lib/sentry';
55

66
import IdlPageClient from './page-client';
77

@@ -12,7 +12,7 @@ type Props = Readonly<{
1212
}>;
1313

1414
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
15-
return withSentryTraceData({
15+
return withTraceData({
1616
description: `The Interface Definition Language (IDL) file for the program at address ${props.params.address} on Solana`,
1717
title: `Program IDL | ${await getReadableTitleFromAddress(props)} | Solana`,
1818
});
Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
'use client';
22

3-
import { captureException } from '@sentry/nextjs';
43
import React from 'react';
5-
import { ErrorBoundary } from 'react-error-boundary';
64

75
import { ParsedAccountRenderer } from '@/app/components/account/ParsedAccountRenderer';
8-
import { ErrorCard } from '@/app/components/common/ErrorCard';
96
import { SecurityCard } from '@/app/features/security-txt/ui/SecurityCard';
107

11-
const isSentryEnabled = process.env.NEXT_PUBLIC_ENABLE_CATCH_EXCEPTIONS === '1';
12-
138
type Props = Readonly<{
149
params: {
1510
address: string;
@@ -28,16 +23,5 @@ function SecurityCardRenderer({
2823
}
2924

3025
export default function SecurityPageClient({ params: { address } }: Props) {
31-
return (
32-
<ErrorBoundary
33-
onError={(error: Error) => {
34-
if (isSentryEnabled) {
35-
captureException(error);
36-
}
37-
}}
38-
fallbackRender={({ error }) => <ErrorCard text={`Failed to load security data: ${error.message}`} />}
39-
>
40-
<ParsedAccountRenderer address={address} renderComponent={SecurityCardRenderer} />
41-
</ErrorBoundary>
42-
);
26+
return <ParsedAccountRenderer address={address} renderComponent={SecurityCardRenderer} />;
4327
}

app/address/[address]/security/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
22
import { Metadata } from 'next/types';
33

4-
import { withSentryTraceData } from '@/app/utils/with-sentry-trace-data';
4+
import { withTraceData } from '@/app/shared/lib/sentry';
55

66
import SecurityPageClient from './page-client';
77

88
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
9-
return withSentryTraceData({
9+
return withTraceData({
1010
description: `Contents of the security.txt for the program with address ${props.params.address} on Solana`,
1111
title: `Security | ${await getReadableTitleFromAddress(props)} | Solana`,
1212
});

app/api/ans-domains/[address]/__tests__/route.spec.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
import { fetchAnsDomains } from '@entities/domain/api/fetch-ans-domains';
2-
import Logger from '@utils/logger';
32
import { beforeEach, describe, expect, it, vi } from 'vitest';
43

4+
import { Logger } from '@/app/shared/lib/logger';
5+
56
import { GET } from '../route';
67

78
vi.mock('@entities/domain/api/fetch-ans-domains', () => ({
89
fetchAnsDomains: vi.fn(),
910
}));
1011

11-
vi.mock('@utils/logger', () => ({
12-
default: {
13-
error: vi.fn(),
14-
},
15-
}));
16-
1712
const VALID_ADDRESS = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
1813
const mockRequest = new Request('http://localhost:3000/api/ans-domains/' + VALID_ADDRESS);
1914

@@ -83,7 +78,7 @@ describe('GET /api/ans-domains/[address]', () => {
8378

8479
await GET(mockRequest, { params: { address: VALID_ADDRESS } });
8580

86-
expect(Logger.error).toHaveBeenCalledWith(error, `Failed to fetch ANS domains for ${VALID_ADDRESS}`);
81+
expect(Logger.error).toHaveBeenCalledWith(error, { address: VALID_ADDRESS });
8782
});
8883

8984
it('should not cache error responses', async () => {

app/api/ans-domains/[address]/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { fetchAnsDomains } from '@entities/domain/api/fetch-ans-domains';
22
import { PublicKey } from '@solana/web3.js';
3-
import Logger from '@utils/logger';
43
import { NextResponse } from 'next/server';
54

5+
import { Logger } from '@/app/shared/lib/logger';
6+
67
const CACHE_HEADERS = { 'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate=3600' };
78

89
type Params = {
@@ -22,7 +23,7 @@ export async function GET(_request: Request, { params: { address } }: Params) {
2223
const domains = await fetchAnsDomains(address);
2324
return NextResponse.json({ domains }, { headers: CACHE_HEADERS });
2425
} catch (error) {
25-
Logger.error(error, `Failed to fetch ANS domains for ${address}`);
26+
Logger.error(error, { address });
2627
return NextResponse.json({ domains: [] }, { headers: { 'Cache-Control': 'no-store' }, status: 500 });
2728
}
2829
}

0 commit comments

Comments
 (0)