sh-ui-cli 0.97.0 → 0.98.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/data/changelog/versions.json +25 -0
- package/package.json +1 -1
- package/src/add.mjs +39 -14
- package/src/api.d.ts +0 -2
- package/src/constants.js +0 -5
- package/src/create/cli-args.js +1 -5
- package/src/create/describeTemplate.js +2 -31
- package/src/create/generator.js +9 -665
- package/src/create/index.mjs +2 -5
- package/src/create/plugins/index.js +1 -3
- package/src/create/plugins/nextIntl.js +0 -4
- package/src/create/plugins/pluginSchema.js +1 -1
- package/src/css-bundle.mjs +60 -0
- package/src/mcp.mjs +0 -34
- package/src/remove.mjs +10 -1
- package/src/create/plugins/authJwt.js +0 -420
- package/src/create/plugins/sentry.js +0 -467
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
export const sentryPlugin = {
|
|
2
|
-
name: 'sentry',
|
|
3
|
-
label: 'Sentry (에러 모니터링)',
|
|
4
|
-
description:
|
|
5
|
-
'에러 모니터링. 클라/서버/엣지 init, 라우트 에러 페이지, observability 브릿지로 다른 플러그인의 5xx 자동 캡처.',
|
|
6
|
-
priority: 1,
|
|
7
|
-
|
|
8
|
-
dependencies: {
|
|
9
|
-
'@sentry/nextjs': '^10.44.0',
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
// ─── next.config.ts 관련 ───
|
|
13
|
-
|
|
14
|
-
imports: [
|
|
15
|
-
`import { withSentryConfig } from '@sentry/nextjs';`,
|
|
16
|
-
],
|
|
17
|
-
|
|
18
|
-
wrapExport(expr) {
|
|
19
|
-
return `withSentryConfig(${expr}, {
|
|
20
|
-
org: process.env.SENTRY_ORG,
|
|
21
|
-
project: process.env.SENTRY_PROJECT,
|
|
22
|
-
authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
23
|
-
tunnelRoute: '/monitoring',
|
|
24
|
-
silent: !process.env.CI,
|
|
25
|
-
bundleSizeOptimizations: {
|
|
26
|
-
excludeDebugStatements: true,
|
|
27
|
-
},
|
|
28
|
-
})`;
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
envVars: [
|
|
32
|
-
'# Sentry',
|
|
33
|
-
'SENTRY_ORG=',
|
|
34
|
-
'SENTRY_PROJECT=',
|
|
35
|
-
'SENTRY_AUTH_TOKEN=',
|
|
36
|
-
'NEXT_PUBLIC_SENTRY_DSN=',
|
|
37
|
-
'NEXT_PUBLIC_SENTRY_ENVIRONMENT=dev',
|
|
38
|
-
],
|
|
39
|
-
|
|
40
|
-
turboEnvVars: [
|
|
41
|
-
'NEXT_PUBLIC_SENTRY_DSN',
|
|
42
|
-
'NEXT_PUBLIC_SENTRY_ENVIRONMENT',
|
|
43
|
-
'NEXT_RUNTIME',
|
|
44
|
-
'SENTRY_ORG',
|
|
45
|
-
'SENTRY_PROJECT',
|
|
46
|
-
'SENTRY_AUTH_TOKEN',
|
|
47
|
-
],
|
|
48
|
-
|
|
49
|
-
// ─── 공유 파일 조각 (providers 합성용) ───
|
|
50
|
-
|
|
51
|
-
providerImports: [],
|
|
52
|
-
providerWrappers: [],
|
|
53
|
-
|
|
54
|
-
// ─── 독립 파일 ───
|
|
55
|
-
//
|
|
56
|
-
// Sentry 가 활성화될 때만 깔리는 파일.
|
|
57
|
-
// HTTP/proxy 인프라(http.ts, apiTypes.ts, error.ts, app/api/proxy 등)는
|
|
58
|
-
// 베이스 템플릿이 소유한다. Sentry 는 베이스의 observability.ts 를
|
|
59
|
-
// Sentry-aware 버전으로 덮어써서 캡처/로그를 활성화한다.
|
|
60
|
-
//
|
|
61
|
-
// arch 의존: FallbackBoundary 는 arch.paths.ui 에, observability 는 arch.paths.api 에
|
|
62
|
-
// 떨어진다. FallbackBoundary 안의 ApiError import 는 arch.aliases.api 로 fully-qualified —
|
|
63
|
-
// 상대 경로 (`../../api/error`) 는 FSD 에서만 동작하므로 alias 로 통일.
|
|
64
|
-
|
|
65
|
-
files: (arch) => ({
|
|
66
|
-
'sentry.server.config.ts': `import * as Sentry from '@sentry/nextjs';
|
|
67
|
-
|
|
68
|
-
import { ApiError } from '${arch.aliases.api}/error';
|
|
69
|
-
|
|
70
|
-
Sentry.init({
|
|
71
|
-
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
72
|
-
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? 'dev',
|
|
73
|
-
enabled: !!process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
74
|
-
tracesSampleRate: 0,
|
|
75
|
-
beforeSend: (event, hint) => {
|
|
76
|
-
if (event.level === 'warning' || event.level === 'info' || event.level === 'debug' || event.level === 'log') {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const error = hint?.originalException;
|
|
81
|
-
|
|
82
|
-
// 401 (인증 실패) 은 비즈니스 흐름 — Sentry 로 안 보냄.
|
|
83
|
-
if (error instanceof ApiError && error.status === 401) return null;
|
|
84
|
-
|
|
85
|
-
if (event.exception?.values?.[0]?.value?.includes('An error occurred in the Server Components render')) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return event;
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
`,
|
|
93
|
-
|
|
94
|
-
'sentry.edge.config.ts': `import * as Sentry from '@sentry/nextjs';
|
|
95
|
-
|
|
96
|
-
Sentry.init({
|
|
97
|
-
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
98
|
-
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? 'dev',
|
|
99
|
-
enabled: !!process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
100
|
-
tracesSampleRate: 0,
|
|
101
|
-
beforeSend: (event) => {
|
|
102
|
-
if (event.level === 'warning' || event.level === 'info' || event.level === 'debug' || event.level === 'log') {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
return event;
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
`,
|
|
109
|
-
|
|
110
|
-
'instrumentation.ts': `import * as Sentry from '@sentry/nextjs';
|
|
111
|
-
|
|
112
|
-
export const register = async () => {
|
|
113
|
-
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
114
|
-
await import('./sentry.server.config');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (process.env.NEXT_RUNTIME === 'edge') {
|
|
118
|
-
await import('./sentry.edge.config');
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export const onRequestError = (
|
|
123
|
-
...[error, request, context]: Parameters<typeof Sentry.captureRequestError>
|
|
124
|
-
) => {
|
|
125
|
-
if (error instanceof Error && error.name === 'ApiError') {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
Sentry.captureRequestError(error, request, context);
|
|
130
|
-
};
|
|
131
|
-
`,
|
|
132
|
-
|
|
133
|
-
'instrumentation-client.ts': `import * as Sentry from '@sentry/nextjs';
|
|
134
|
-
|
|
135
|
-
const IGNORED_ERROR_PATTERNS = [
|
|
136
|
-
'ResizeObserver loop',
|
|
137
|
-
'ResizeObserver loop completed with undelivered notifications',
|
|
138
|
-
'Non-Error promise rejection captured',
|
|
139
|
-
'AbortError',
|
|
140
|
-
'ChunkLoadError',
|
|
141
|
-
'Loading chunk',
|
|
142
|
-
];
|
|
143
|
-
|
|
144
|
-
const BROWSER_NETWORK_ERROR_PATTERNS = [
|
|
145
|
-
'Load failed',
|
|
146
|
-
'Failed to fetch',
|
|
147
|
-
'NetworkError',
|
|
148
|
-
];
|
|
149
|
-
|
|
150
|
-
Sentry.init({
|
|
151
|
-
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
152
|
-
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? 'dev',
|
|
153
|
-
enabled: !!process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
154
|
-
tracesSampleRate: 0,
|
|
155
|
-
integrations: [Sentry.replayIntegration()],
|
|
156
|
-
replaysSessionSampleRate: 0,
|
|
157
|
-
replaysOnErrorSampleRate: 0.5,
|
|
158
|
-
ignoreErrors: IGNORED_ERROR_PATTERNS,
|
|
159
|
-
denyUrls: [
|
|
160
|
-
/extensions\\//i,
|
|
161
|
-
/chrome-extension:/i,
|
|
162
|
-
/safari-web-extension:/i,
|
|
163
|
-
],
|
|
164
|
-
beforeSend: (event, hint) => {
|
|
165
|
-
if (event.level === 'warning' || event.level === 'info' || event.level === 'debug' || event.level === 'log') {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const error = hint?.originalException;
|
|
170
|
-
|
|
171
|
-
if (error instanceof Error && error.name === 'ApiError') {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
error instanceof Error &&
|
|
177
|
-
BROWSER_NETWORK_ERROR_PATTERNS.some((pattern) => error.message.includes(pattern))
|
|
178
|
-
) {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return event;
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
|
187
|
-
`,
|
|
188
|
-
|
|
189
|
-
// global-error.tsx — root layout 자체가 깨졌을 때의 fallback. Tailwind/CSS 가 로드 안 될 수
|
|
190
|
-
// 있어 inline style 위주. 한국어로 통일 ([locale]/error.tsx 와 동일 메시지 톤).
|
|
191
|
-
'app/global-error.tsx': `'use client';
|
|
192
|
-
|
|
193
|
-
import * as Sentry from '@sentry/nextjs';
|
|
194
|
-
import { useEffect } from 'react';
|
|
195
|
-
|
|
196
|
-
export default function GlobalError({
|
|
197
|
-
error,
|
|
198
|
-
}: {
|
|
199
|
-
error: Error & { digest?: string };
|
|
200
|
-
}) {
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
Sentry.captureException(error);
|
|
203
|
-
}, [error]);
|
|
204
|
-
|
|
205
|
-
return (
|
|
206
|
-
<html lang='ko'>
|
|
207
|
-
<body
|
|
208
|
-
style={{
|
|
209
|
-
margin: 0,
|
|
210
|
-
minHeight: '100vh',
|
|
211
|
-
display: 'flex',
|
|
212
|
-
alignItems: 'center',
|
|
213
|
-
justifyContent: 'center',
|
|
214
|
-
fontFamily:
|
|
215
|
-
'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
216
|
-
}}
|
|
217
|
-
>
|
|
218
|
-
<div style={{ textAlign: 'center', padding: 24 }}>
|
|
219
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, margin: 0 }}>
|
|
220
|
-
오류가 발생했습니다
|
|
221
|
-
</h1>
|
|
222
|
-
<p style={{ marginTop: 8, color: '#6b7280' }}>{error.message}</p>
|
|
223
|
-
</div>
|
|
224
|
-
</body>
|
|
225
|
-
</html>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
`,
|
|
229
|
-
|
|
230
|
-
// [locale] 안으로 이동 시 next-intl 플러그인이 i18n-aware 버전으로 replace 함.
|
|
231
|
-
// 여기 emit 되는 버전은 next-intl 미활성 케이스용 — 한국어 + 토큰 기반 색상.
|
|
232
|
-
// \`bg-accent\` 같은 미정의 토큰은 사용하지 않고 \`--background-muted\` / \`--danger\`
|
|
233
|
-
// 등 tokens.css 에 실재하는 변수만 사용.
|
|
234
|
-
'app/error.tsx': `'use client';
|
|
235
|
-
|
|
236
|
-
import * as Sentry from '@sentry/nextjs';
|
|
237
|
-
import { AlertTriangle, Home, RefreshCw } from 'lucide-react';
|
|
238
|
-
import Link from 'next/link';
|
|
239
|
-
import { useEffect } from 'react';
|
|
240
|
-
|
|
241
|
-
export default function Error({
|
|
242
|
-
error,
|
|
243
|
-
reset,
|
|
244
|
-
}: {
|
|
245
|
-
error: Error & { digest?: string };
|
|
246
|
-
reset: () => void;
|
|
247
|
-
}) {
|
|
248
|
-
useEffect(() => {
|
|
249
|
-
Sentry.captureException(error);
|
|
250
|
-
}, [error]);
|
|
251
|
-
|
|
252
|
-
return (
|
|
253
|
-
<div className='flex min-h-screen items-center justify-center px-4'>
|
|
254
|
-
<div className='border-border bg-background w-full max-w-md rounded-lg border p-6 shadow-lg'>
|
|
255
|
-
<div className='mb-4 flex justify-center'>
|
|
256
|
-
<div className='bg-danger/10 flex h-16 w-16 items-center justify-center rounded-full'>
|
|
257
|
-
<AlertTriangle className='text-danger h-8 w-8' />
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
|
-
<h2 className='text-foreground mb-2 text-center text-2xl font-bold'>
|
|
262
|
-
오류가 발생했습니다
|
|
263
|
-
</h2>
|
|
264
|
-
<p className='text-foreground-muted mb-6 text-center text-sm'>
|
|
265
|
-
예상치 못한 오류가 발생했습니다. 다시 시도해주세요.
|
|
266
|
-
</p>
|
|
267
|
-
|
|
268
|
-
<div className='border-danger/30 bg-danger/5 rounded-md border p-3'>
|
|
269
|
-
<p className='text-danger text-sm'>
|
|
270
|
-
{error.message || '알 수 없는 오류'}
|
|
271
|
-
</p>
|
|
272
|
-
</div>
|
|
273
|
-
|
|
274
|
-
<div className='mt-6 space-y-3'>
|
|
275
|
-
<button
|
|
276
|
-
onClick={reset}
|
|
277
|
-
className='bg-primary text-primary-foreground hover:bg-primary-hover flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium'
|
|
278
|
-
>
|
|
279
|
-
<RefreshCw className='h-4 w-4' />
|
|
280
|
-
다시 시도
|
|
281
|
-
</button>
|
|
282
|
-
|
|
283
|
-
<Link
|
|
284
|
-
href='/'
|
|
285
|
-
className='border-border text-foreground hover:bg-background-muted flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium'
|
|
286
|
-
>
|
|
287
|
-
<Home className='h-4 w-4' />
|
|
288
|
-
홈으로 이동
|
|
289
|
-
</Link>
|
|
290
|
-
</div>
|
|
291
|
-
|
|
292
|
-
{process.env.NODE_ENV === 'development' && error.digest && (
|
|
293
|
-
<div className='bg-background-subtle mt-4 rounded-md p-3'>
|
|
294
|
-
<p className='text-foreground-subtle text-xs'>
|
|
295
|
-
Error ID: {error.digest}
|
|
296
|
-
</p>
|
|
297
|
-
</div>
|
|
298
|
-
)}
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
`,
|
|
304
|
-
|
|
305
|
-
[`${arch.paths.ui}/FallbackBoundary/index.tsx`]: `import React, {
|
|
306
|
-
Component,
|
|
307
|
-
ComponentType,
|
|
308
|
-
ErrorInfo,
|
|
309
|
-
ReactNode,
|
|
310
|
-
Suspense,
|
|
311
|
-
} from 'react';
|
|
312
|
-
import * as Sentry from '@sentry/nextjs';
|
|
313
|
-
import { QueryErrorResetBoundary } from '@tanstack/react-query';
|
|
314
|
-
|
|
315
|
-
import { ApiError } from '${arch.aliases.api}/error';
|
|
316
|
-
|
|
317
|
-
interface ErrorFallbackProps {
|
|
318
|
-
error: Error | null;
|
|
319
|
-
resetErrorBoundary: () => void;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
interface ErrorBoundaryProps {
|
|
323
|
-
children: ReactNode;
|
|
324
|
-
fallback?: React.ElementType<ErrorFallbackProps>;
|
|
325
|
-
onReset: () => void;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
interface ErrorBoundaryState {
|
|
329
|
-
hasError: boolean;
|
|
330
|
-
error: Error | null;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
334
|
-
state: ErrorBoundaryState = { hasError: false, error: null };
|
|
335
|
-
|
|
336
|
-
constructor(props: ErrorBoundaryProps) {
|
|
337
|
-
super(props);
|
|
338
|
-
this.resetErrorBoundary = this.resetErrorBoundary.bind(this);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
342
|
-
return { hasError: true, error };
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
346
|
-
if (error instanceof ApiError) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
Sentry.withScope((scope) => {
|
|
351
|
-
scope.setTag('boundary', 'ErrorBoundary');
|
|
352
|
-
scope.setFingerprint(['ErrorBoundary', error.name, error.message]);
|
|
353
|
-
|
|
354
|
-
if (errorInfo.componentStack) {
|
|
355
|
-
scope.setContext('react', {
|
|
356
|
-
componentStack: errorInfo.componentStack,
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
Sentry.captureException(error);
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
resetErrorBoundary() {
|
|
364
|
-
this.props.onReset();
|
|
365
|
-
this.setState({ hasError: false, error: null });
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
render() {
|
|
369
|
-
const { hasError, error } = this.state;
|
|
370
|
-
const { children, fallback: Fallback } = this.props;
|
|
371
|
-
|
|
372
|
-
if (hasError && Fallback) {
|
|
373
|
-
return (
|
|
374
|
-
<Fallback error={error} resetErrorBoundary={this.resetErrorBoundary} />
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return children;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
interface FallbackBoundaryProps {
|
|
383
|
-
children: ReactNode;
|
|
384
|
-
errorFallback?: ComponentType<ErrorFallbackProps>;
|
|
385
|
-
suspenseFallback?: ReactNode;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
export function FallbackBoundary({
|
|
389
|
-
children,
|
|
390
|
-
errorFallback,
|
|
391
|
-
suspenseFallback,
|
|
392
|
-
}: FallbackBoundaryProps) {
|
|
393
|
-
return (
|
|
394
|
-
<QueryErrorResetBoundary>
|
|
395
|
-
{({ reset }) => (
|
|
396
|
-
<ErrorBoundary onReset={reset} fallback={errorFallback}>
|
|
397
|
-
<Suspense fallback={suspenseFallback}>{children}</Suspense>
|
|
398
|
-
</ErrorBoundary>
|
|
399
|
-
)}
|
|
400
|
-
</QueryErrorResetBoundary>
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
`,
|
|
404
|
-
|
|
405
|
-
// ─── observability 브릿지 ───
|
|
406
|
-
//
|
|
407
|
-
// 베이스의 no-op observability 를 Sentry-aware 버전으로 덮어쓴다.
|
|
408
|
-
// http.ts / serverFetch.ts / proxy/route.ts 가 이 모듈을 import 하므로,
|
|
409
|
-
// Sentry 플러그인이 켜지면 자동으로 캡처가 활성화된다.
|
|
410
|
-
|
|
411
|
-
[`${arch.paths.api}/observability.ts`]: `import * as Sentry from '@sentry/nextjs';
|
|
412
|
-
|
|
413
|
-
type ApiCaptureParams = {
|
|
414
|
-
url: string;
|
|
415
|
-
apiPath: string;
|
|
416
|
-
method: string;
|
|
417
|
-
status: number | undefined;
|
|
418
|
-
responseBody?: unknown;
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
type ApiLogParams = {
|
|
422
|
-
url: string;
|
|
423
|
-
method: string;
|
|
424
|
-
status: number | undefined;
|
|
425
|
-
requestHeaders?: Record<string, string | undefined>;
|
|
426
|
-
requestBody?: unknown;
|
|
427
|
-
responseBody?: unknown;
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
export const captureApiError = (params: ApiCaptureParams): void => {
|
|
431
|
-
const { url, apiPath, method, status, responseBody } = params;
|
|
432
|
-
|
|
433
|
-
// 5xx 서버 에러만 Sentry 로 보고 (4xx 비즈니스 에러는 UI 에서 처리)
|
|
434
|
-
if (!status || status < 500) return;
|
|
435
|
-
|
|
436
|
-
Sentry.withScope((scope) => {
|
|
437
|
-
scope.setTag('error.type', 'api');
|
|
438
|
-
scope.setContext('API Request', { method, url });
|
|
439
|
-
scope.setContext('API Response', {
|
|
440
|
-
status,
|
|
441
|
-
body: JSON.stringify(responseBody),
|
|
442
|
-
});
|
|
443
|
-
scope.setFingerprint([method, apiPath, String(status)]);
|
|
444
|
-
|
|
445
|
-
Sentry.captureException(new Error(\`[API] \${method} \${apiPath} \${status}\`));
|
|
446
|
-
});
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
export const logApiError = (prefix: string, params: ApiLogParams): void => {
|
|
450
|
-
const { url, method, status, requestHeaders, requestBody, responseBody } = params;
|
|
451
|
-
|
|
452
|
-
console.error(\`❌ [\${prefix} ERROR REPORT]\`);
|
|
453
|
-
console.error(\`- URL: \${method} \${url}\`);
|
|
454
|
-
console.error(\`- Status: \${status ?? 'N/A'}\`);
|
|
455
|
-
|
|
456
|
-
if (requestHeaders) {
|
|
457
|
-
// Authorization 토큰은 로그에서 가린다.
|
|
458
|
-
const { Authorization: _authorization, ...safeHeaders } = requestHeaders;
|
|
459
|
-
void _authorization;
|
|
460
|
-
console.error('- Request Headers:', safeHeaders);
|
|
461
|
-
}
|
|
462
|
-
if (requestBody) console.error('- Request Body:', requestBody);
|
|
463
|
-
if (responseBody) console.error('- Response Body:', responseBody);
|
|
464
|
-
};
|
|
465
|
-
`,
|
|
466
|
-
}),
|
|
467
|
-
};
|