add sentry
This commit is contained in:
parent
7ba2926da0
commit
9dcc23a180
3
.gitignore
vendored
3
.gitignore
vendored
@ -39,3 +39,6 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# Sentry Config File
|
||||||
|
.env.sentry-build-plugin
|
||||||
|
|||||||
130
CLAUDE.md
130
CLAUDE.md
@ -91,3 +91,133 @@ This is a Next.js 15 application built with React 19 that creates an interactive
|
|||||||
- `public/` - Static assets
|
- `public/` - Static assets
|
||||||
|
|
||||||
The application combines modern web mapping with custom WebGL visualization to create an interactive timeline-driven map interface, complemented by a full-featured admin system for content management.
|
The application combines modern web mapping with custom WebGL visualization to create an interactive timeline-driven map interface, complemented by a full-featured admin system for content management.
|
||||||
|
|
||||||
|
|
||||||
|
These examples should be used as guidance when configuring Sentry functionality within a project.
|
||||||
|
|
||||||
|
# Exception Catching
|
||||||
|
|
||||||
|
Use `Sentry.captureException(error)` to capture an exception and log the error in Sentry.
|
||||||
|
Use this in try catch blocks or areas where exceptions are expected
|
||||||
|
|
||||||
|
# Tracing Examples
|
||||||
|
|
||||||
|
Spans should be created for meaningful actions within an applications like button clicks, API calls, and function calls
|
||||||
|
Use the `Sentry.startSpan` function to create a span
|
||||||
|
Child spans can exist within a parent span
|
||||||
|
|
||||||
|
## Custom Span instrumentation in component actions
|
||||||
|
|
||||||
|
The `name` and `op` properties should be meaninful for the activities in the call.
|
||||||
|
Attach attributes based on relevant information and metrics from the request
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function TestComponent() {
|
||||||
|
const handleTestButtonClick = () => {
|
||||||
|
// Create a transaction/span to measure performance
|
||||||
|
Sentry.startSpan(
|
||||||
|
{
|
||||||
|
op: "ui.click",
|
||||||
|
name: "Test Button Click",
|
||||||
|
},
|
||||||
|
(span) => {
|
||||||
|
const value = "some config";
|
||||||
|
const metric = "some metric";
|
||||||
|
|
||||||
|
// Metrics can be added to the span
|
||||||
|
span.setAttribute("config", value);
|
||||||
|
span.setAttribute("metric", metric);
|
||||||
|
|
||||||
|
doSomething();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button type="button" onClick={handleTestButtonClick}>
|
||||||
|
Test Sentry
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom span instrumentation in API calls
|
||||||
|
|
||||||
|
The `name` and `op` properties should be meaninful for the activities in the call.
|
||||||
|
Attach attributes based on relevant information and metrics from the request
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function fetchUserData(userId) {
|
||||||
|
return Sentry.startSpan(
|
||||||
|
{
|
||||||
|
op: "http.client",
|
||||||
|
name: `GET /api/users/${userId}`,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const response = await fetch(`/api/users/${userId}`);
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
Where logs are used, ensure Sentry is imported using `import * as Sentry from "@sentry/nextjs"`
|
||||||
|
Enable logging in Sentry using `Sentry.init({ _experiments: { enableLogs: true } })`
|
||||||
|
Reference the logger using `const { logger } = Sentry`
|
||||||
|
Sentry offers a consoleLoggingIntegration that can be used to log specific console error types automatically without instrumenting the individual logger calls
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
In NextJS the client side Sentry initialization is in `instrumentation-client.ts`, the server initialization is in `sentry.edge.config.ts` and the edge initialization is in `sentry.server.config.ts`
|
||||||
|
Initialization does not need to be repeated in other files, it only needs to happen the files mentioned above. You should use `import * as Sentry from "@sentry/nextjs"` to reference Sentry functionality
|
||||||
|
|
||||||
|
### Baseline
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://109bcfcc2d1cdd643e0af61409016900@o4505647824109568.ingest.us.sentry.io/4509868655181824",
|
||||||
|
|
||||||
|
_experiments: {
|
||||||
|
enableLogs: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logger Integration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://109bcfcc2d1cdd643e0af61409016900@o4505647824109568.ingest.us.sentry.io/4509868655181824",
|
||||||
|
integrations: [
|
||||||
|
// send console.log, console.warn, and console.error calls as logs to Sentry
|
||||||
|
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logger Examples
|
||||||
|
|
||||||
|
`logger.fmt` is a template literal function that should be used to bring variables into the structured logs.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
logger.trace("Starting database connection", { database: "users" });
|
||||||
|
logger.debug(logger.fmt`Cache miss for user: ${userId}`);
|
||||||
|
logger.info("Updated profile", { profileId: 345 });
|
||||||
|
logger.warn("Rate limit reached for endpoint", {
|
||||||
|
endpoint: "/api/results/",
|
||||||
|
isEnterprise: false,
|
||||||
|
});
|
||||||
|
logger.error("Failed to process payment", {
|
||||||
|
orderId: "order_123",
|
||||||
|
amount: 99.99,
|
||||||
|
});
|
||||||
|
logger.fatal("Database connection pool exhausted", {
|
||||||
|
database: "users",
|
||||||
|
activeConnections: 100,
|
||||||
|
});
|
||||||
|
```
|
||||||
14
app/api/sentry-example-api/route.ts
Normal file
14
app/api/sentry-example-api/route.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
class SentryExampleAPIError extends Error {
|
||||||
|
constructor(message: string | undefined) {
|
||||||
|
super(message);
|
||||||
|
this.name = "SentryExampleAPIError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A faulty API route to test Sentry's error monitoring
|
||||||
|
export function GET() {
|
||||||
|
throw new SentryExampleAPIError("This error is raised on the backend called by the example page.");
|
||||||
|
return NextResponse.json({ data: "Testing Sentry Error..." });
|
||||||
|
}
|
||||||
23
app/global-error.tsx
Normal file
23
app/global-error.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import NextError from "next/error";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
|
||||||
|
useEffect(() => {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
{/* `NextError` is the default Next.js error page component. Its type
|
||||||
|
definition requires a `statusCode` prop. However, since the App Router
|
||||||
|
does not expose status codes for errors, we simply pass 0 to render a
|
||||||
|
generic error message. */}
|
||||||
|
<NextError statusCode={0} />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -11,6 +11,8 @@ import { useRadarTile } from '@/hooks/use-radartile'
|
|||||||
import { format, formatInTimeZone } from 'date-fns-tz'
|
import { format, formatInTimeZone } from 'date-fns-tz'
|
||||||
import vertexSource from '@/app/glsl/radar/verx.glsl'
|
import vertexSource from '@/app/glsl/radar/verx.glsl'
|
||||||
import fragmentSource from '@/app/glsl/radar/frag.glsl'
|
import fragmentSource from '@/app/glsl/radar/frag.glsl'
|
||||||
|
import * as Sentry from '@sentry/nextjs'
|
||||||
|
import { logger, logWebGLError, logMapEvent, logPerformanceMetric } from '@/lib/logger'
|
||||||
|
|
||||||
interface MapComponentProps {
|
interface MapComponentProps {
|
||||||
style?: string
|
style?: string
|
||||||
@ -66,6 +68,15 @@ export function MapComponent({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapContainer.current) return
|
if (!mapContainer.current) return
|
||||||
|
|
||||||
|
const span = Sentry.startInactiveSpan({
|
||||||
|
op: "ui.component.load",
|
||||||
|
name: "Map Component Initialization",
|
||||||
|
});
|
||||||
|
|
||||||
|
span.setAttribute("map.style", style);
|
||||||
|
span.setAttribute("map.center", `${location.center[0]},${location.center[1]}`);
|
||||||
|
span.setAttribute("map.zoom", location.zoom);
|
||||||
|
|
||||||
const map = new maplibregl.Map({
|
const map = new maplibregl.Map({
|
||||||
container: mapContainer.current,
|
container: mapContainer.current,
|
||||||
style: style,
|
style: style,
|
||||||
@ -81,6 +92,17 @@ export function MapComponent({
|
|||||||
|
|
||||||
|
|
||||||
map.on('style.load', () => {
|
map.on('style.load', () => {
|
||||||
|
logMapEvent('style.load', {
|
||||||
|
style: style,
|
||||||
|
center: location.center,
|
||||||
|
zoom: location.zoom
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Map style loaded successfully', {
|
||||||
|
component: 'MapComponent',
|
||||||
|
style: style
|
||||||
|
});
|
||||||
|
|
||||||
map.setProjection({
|
map.setProjection({
|
||||||
type: 'globe'
|
type: 'globe'
|
||||||
})
|
})
|
||||||
@ -103,13 +125,24 @@ export function MapComponent({
|
|||||||
// Helper function to compile shader
|
// Helper function to compile shader
|
||||||
const compileShader = (source: string, type: number): WebGLShader | null => {
|
const compileShader = (source: string, type: number): WebGLShader | null => {
|
||||||
const shader = gl.createShader(type);
|
const shader = gl.createShader(type);
|
||||||
if (!shader) return null;
|
if (!shader) {
|
||||||
|
const error = new Error('Failed to create WebGL shader');
|
||||||
|
Sentry.captureException(error);
|
||||||
|
logWebGLError('shader_creation', 'Failed to create WebGL shader', { shaderType: type === gl.VERTEX_SHADER ? 'vertex' : 'fragment' });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
gl.shaderSource(shader, source);
|
gl.shaderSource(shader, source);
|
||||||
gl.compileShader(shader);
|
gl.compileShader(shader);
|
||||||
|
|
||||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
|
const errorLog = gl.getShaderInfoLog(shader);
|
||||||
|
const error = new Error(`Shader compilation failed: ${errorLog}`);
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: { component: 'MapComponent', operation: 'shader_compilation' },
|
||||||
|
extra: { shaderType: type === gl.VERTEX_SHADER ? 'vertex' : 'fragment', source }
|
||||||
|
});
|
||||||
|
logWebGLError('shader_compilation', errorLog, { shaderType: type === gl.VERTEX_SHADER ? 'vertex' : 'fragment' });
|
||||||
gl.deleteShader(shader);
|
gl.deleteShader(shader);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -131,7 +164,13 @@ export function MapComponent({
|
|||||||
gl.linkProgram(program);
|
gl.linkProgram(program);
|
||||||
|
|
||||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||||
console.error('Program link error:', gl.getProgramInfoLog(program));
|
const errorLog = gl.getProgramInfoLog(program);
|
||||||
|
const error = new Error(`WebGL program linking failed: ${errorLog}`);
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: { component: 'MapComponent', operation: 'program_linking' },
|
||||||
|
extra: { programLog: errorLog }
|
||||||
|
});
|
||||||
|
logWebGLError('program_linking', errorLog);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +183,11 @@ export function MapComponent({
|
|||||||
const tex = gl.createTexture()
|
const tex = gl.createTexture()
|
||||||
|
|
||||||
if (!tex) {
|
if (!tex) {
|
||||||
console.error('Failed to create texture');
|
const error = new Error('Failed to create WebGL texture');
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: { component: 'MapComponent', operation: 'texture_creation' }
|
||||||
|
});
|
||||||
|
logWebGLError('texture_creation', 'Failed to create WebGL texture');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,7 +285,10 @@ export function MapComponent({
|
|||||||
// 只在缩放级别变化时更新网格数据
|
// 只在缩放级别变化时更新网格数据
|
||||||
const currentZoom = Math.floor(map.getZoom());
|
const currentZoom = Math.floor(map.getZoom());
|
||||||
if (currentZoom !== this.lastZoom) {
|
if (currentZoom !== this.lastZoom) {
|
||||||
console.log(`缩放级别变化: ${this.lastZoom} -> ${currentZoom}`);
|
logMapEvent('zoom_change', {
|
||||||
|
previousZoom: this.lastZoom,
|
||||||
|
currentZoom: currentZoom
|
||||||
|
});
|
||||||
|
|
||||||
// 智能计算最佳细分数量
|
// 智能计算最佳细分数量
|
||||||
const performanceLevel = detectPerformanceLevel();
|
const performanceLevel = detectPerformanceLevel();
|
||||||
@ -254,9 +300,22 @@ export function MapComponent({
|
|||||||
|
|
||||||
// 获取细分建议信息
|
// 获取细分建议信息
|
||||||
const recommendation = getSubdivisionRecommendation(currentZoom, performanceLevel);
|
const recommendation = getSubdivisionRecommendation(currentZoom, performanceLevel);
|
||||||
console.log(`缩放级别: ${currentZoom}, 性能等级: ${performanceLevel}`);
|
|
||||||
console.log(`细分建议: ${recommendation.subdivisions} (${recommendation.description})`);
|
logger.debug(logger.fmt`Zoom level: ${currentZoom}, Performance level: ${performanceLevel}`, {
|
||||||
console.log(`三角形数量: ${recommendation.triangleCount}, 预计内存: ${recommendation.estimatedMemoryMB}MB`);
|
currentZoom,
|
||||||
|
performanceLevel,
|
||||||
|
component: 'MapComponent'
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(logger.fmt`Subdivision recommendation: ${recommendation.subdivisions} (${recommendation.description})`, {
|
||||||
|
subdivisions: recommendation.subdivisions,
|
||||||
|
description: recommendation.description,
|
||||||
|
triangleCount: recommendation.triangleCount,
|
||||||
|
estimatedMemoryMB: recommendation.estimatedMemoryMB
|
||||||
|
});
|
||||||
|
|
||||||
|
logPerformanceMetric('triangles', recommendation.triangleCount, 'count');
|
||||||
|
logPerformanceMetric('memory_estimate', recommendation.estimatedMemoryMB, 'MB');
|
||||||
|
|
||||||
const meshData = RegionMeshPresets.china(currentZoom, 32);
|
const meshData = RegionMeshPresets.china(currentZoom, 32);
|
||||||
|
|
||||||
@ -282,7 +341,10 @@ export function MapComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onAdd: function (map: maplibregl.Map, gl: WebGL2RenderingContext) {
|
onAdd: function (map: maplibregl.Map, gl: WebGL2RenderingContext) {
|
||||||
console.log('Custom layer added');
|
logger.info('WebGL custom layer added successfully', {
|
||||||
|
component: 'MapComponent',
|
||||||
|
layer: 'custom-gl-layer'
|
||||||
|
});
|
||||||
customLayerRef.current = this;
|
customLayerRef.current = this;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -302,7 +364,10 @@ export function MapComponent({
|
|||||||
if (this.lutTex) gl.deleteTexture(this.lutTex);
|
if (this.lutTex) gl.deleteTexture(this.lutTex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Custom layer resources cleaned up');
|
logger.info('WebGL custom layer resources cleaned up successfully', {
|
||||||
|
component: 'MapComponent',
|
||||||
|
layer: 'custom-gl-layer'
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render(gl: WebGL2RenderingContext | WebGLRenderingContext, { defaultProjectionData }: CustomRenderMethodInput) {
|
render(gl: WebGL2RenderingContext | WebGLRenderingContext, { defaultProjectionData }: CustomRenderMethodInput) {
|
||||||
@ -415,6 +480,9 @@ export function MapComponent({
|
|||||||
if (map) {
|
if (map) {
|
||||||
map.remove();
|
map.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 结束Sentry span
|
||||||
|
span.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [mapContainer])
|
}, [mapContainer])
|
||||||
@ -481,6 +549,15 @@ export function MapComponent({
|
|||||||
|
|
||||||
// 拖动事件处理函数
|
// 拖动事件处理函数
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
Sentry.startSpan(
|
||||||
|
{
|
||||||
|
op: "ui.interaction",
|
||||||
|
name: "Colorbar Drag Start",
|
||||||
|
},
|
||||||
|
(span) => {
|
||||||
|
span.setAttribute("colorbar.position.x", colorbarPosition.x);
|
||||||
|
span.setAttribute("colorbar.position.y", colorbarPosition.y);
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
|
|
||||||
@ -492,6 +569,8 @@ export function MapComponent({
|
|||||||
startPositionY: colorbarPosition.y
|
startPositionY: colorbarPosition.y
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 全局鼠标事件监听
|
// 全局鼠标事件监听
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -97,10 +97,8 @@ export default function TableOfContents({ className }: TableOfContentsProps) {
|
|||||||
|
|
||||||
// 点击目录项滚动到对应位置
|
// 点击目录项滚动到对应位置
|
||||||
const scrollToHeading = useCallback((id: string) => {
|
const scrollToHeading = useCallback((id: string) => {
|
||||||
console.log('Scrolling to:', id); // 调试日志
|
|
||||||
const element = document.getElementById(id);
|
const element = document.getElementById(id);
|
||||||
if (element) {
|
if (element) {
|
||||||
console.log('Element found:', element); // 调试日志
|
|
||||||
const offsetTop = element.getBoundingClientRect().top + window.pageYOffset - 100;
|
const offsetTop = element.getBoundingClientRect().top + window.pageYOffset - 100;
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: offsetTop,
|
top: offsetTop,
|
||||||
@ -110,7 +108,7 @@ export default function TableOfContents({ className }: TableOfContentsProps) {
|
|||||||
// 更新活跃状态
|
// 更新活跃状态
|
||||||
setActiveId(id);
|
setActiveId(id);
|
||||||
} else {
|
} else {
|
||||||
console.log('Element not found for id:', id); // 调试日志
|
// console.log('Element not found for id:', id); // 调试日志
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
34
instrumentation-client.ts
Normal file
34
instrumentation-client.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// This file configures the initialization of Sentry on the client.
|
||||||
|
// The added config here will be used whenever a users loads a page in their browser.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://109bcfcc2d1cdd643e0af61409016900@o4505647824109568.ingest.us.sentry.io/4509868655181824",
|
||||||
|
|
||||||
|
// Add optional integrations for additional features
|
||||||
|
integrations: [
|
||||||
|
Sentry.replayIntegration(),
|
||||||
|
// send console.log, console.warn, and console.error calls as logs to Sentry
|
||||||
|
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||||
|
tracesSampleRate: 1,
|
||||||
|
// Enable logs to be sent to Sentry
|
||||||
|
enableLogs: true,
|
||||||
|
|
||||||
|
// Define how likely Replay events are sampled.
|
||||||
|
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||||
|
// in development and sample at a lower rate in production
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
|
||||||
|
// Define how likely Replay events are sampled when an error occurs.
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
||||||
13
instrumentation.ts
Normal file
13
instrumentation.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import * as Sentry from '@sentry/nextjs';
|
||||||
|
|
||||||
|
export async function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||||
|
await import('./sentry.server.config');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||||
|
await import('./sentry.edge.config');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onRequestError = Sentry.captureRequestError;
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { gql, GraphQLClient } from "graphql-request";
|
import { gql, GraphQLClient } from "graphql-request";
|
||||||
import type { PageData } from "@/types/page";
|
import type { PageData } from "@/types/page";
|
||||||
import { getBaseUrl } from "./gr-client";
|
import { getBaseUrl } from "./gr-client";
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
|
||||||
const PageQuery = gql/* GraphQL */ `
|
const PageQuery = gql/* GraphQL */ `
|
||||||
@ -27,6 +28,15 @@ const BlockQuery = gql/* GraphQL */ `
|
|||||||
|
|
||||||
|
|
||||||
export async function fetchPage(slug: string, jwt?: string): Promise<PageData | null> {
|
export async function fetchPage(slug: string, jwt?: string): Promise<PageData | null> {
|
||||||
|
return Sentry.startSpan(
|
||||||
|
{
|
||||||
|
op: "http.client",
|
||||||
|
name: `GraphQL fetchPage: ${slug}`,
|
||||||
|
},
|
||||||
|
async (span) => {
|
||||||
|
span.setAttribute("page.slug", slug);
|
||||||
|
span.setAttribute("auth.hasJwt", !!jwt);
|
||||||
|
|
||||||
const client = new GraphQLClient(getBaseUrl());
|
const client = new GraphQLClient(getBaseUrl());
|
||||||
|
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
@ -35,21 +45,35 @@ export async function fetchPage(slug: string, jwt?: string): Promise<PageData |
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取页面基本信息
|
// 获取页面基本信息
|
||||||
const pageResponse: any = await client.request(PageQuery, { slug });
|
const pageResponse: any = await Sentry.startSpan(
|
||||||
|
{
|
||||||
|
op: "http.client",
|
||||||
|
name: "GraphQL PageQuery",
|
||||||
|
},
|
||||||
|
() => client.request(PageQuery, { slug })
|
||||||
|
);
|
||||||
|
|
||||||
if (!pageResponse?.pageBySlug) {
|
if (!pageResponse?.pageBySlug) {
|
||||||
throw new Error('Page not found');
|
throw new Error('Page not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取页面块数据
|
// 获取页面块数据
|
||||||
const blocksResponse: any = await client.request(BlockQuery, {
|
const blocksResponse: any = await Sentry.startSpan(
|
||||||
|
{
|
||||||
|
op: "http.client",
|
||||||
|
name: "GraphQL BlockQuery",
|
||||||
|
},
|
||||||
|
() => client.request(BlockQuery, {
|
||||||
pageId: pageResponse.pageBySlug.id
|
pageId: pageResponse.pageBySlug.id
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (!blocksResponse?.pageBlocks) {
|
if (!blocksResponse?.pageBlocks) {
|
||||||
throw new Error('Failed to fetch page blocks');
|
throw new Error('Failed to fetch page blocks');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.setAttribute("page.blocksCount", blocksResponse.pageBlocks.length);
|
||||||
|
|
||||||
// 合并数据
|
// 合并数据
|
||||||
return {
|
return {
|
||||||
...pageResponse.pageBySlug,
|
...pageResponse.pageBySlug,
|
||||||
@ -57,14 +81,28 @@ export async function fetchPage(slug: string, jwt?: string): Promise<PageData |
|
|||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: { operation: 'fetchPage' },
|
||||||
|
extra: { slug, hasJwt: !!jwt }
|
||||||
|
});
|
||||||
console.error('Failed to fetch page:', error);
|
console.error('Failed to fetch page:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSiteConfigs() {
|
export async function getSiteConfigs() {
|
||||||
|
return Sentry.startSpan(
|
||||||
|
{
|
||||||
|
op: "http.client",
|
||||||
|
name: "Fetch Site Configs",
|
||||||
|
},
|
||||||
|
async (span) => {
|
||||||
try {
|
try {
|
||||||
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
|
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
|
||||||
|
span.setAttribute("api.baseUrl", baseUrl);
|
||||||
|
|
||||||
const siteConfigs = await fetch(`${baseUrl}/api/site`, {
|
const siteConfigs = await fetch(`${baseUrl}/api/site`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -73,17 +111,30 @@ export async function getSiteConfigs() {
|
|||||||
signal: AbortSignal.timeout(5000)
|
signal: AbortSignal.timeout(5000)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
span.setAttribute("http.status_code", siteConfigs.status);
|
||||||
|
|
||||||
if (!siteConfigs.ok) {
|
if (!siteConfigs.ok) {
|
||||||
|
const error = new Error(`Failed to fetch site configs: ${siteConfigs.status} ${siteConfigs.statusText}`);
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: { operation: 'getSiteConfigs' },
|
||||||
|
extra: { status: siteConfigs.status, statusText: siteConfigs.statusText }
|
||||||
|
});
|
||||||
console.warn(`Failed to fetch site configs: ${siteConfigs.status} ${siteConfigs.statusText}`);
|
console.warn(`Failed to fetch site configs: ${siteConfigs.status} ${siteConfigs.statusText}`);
|
||||||
return getDefaultSiteConfigs();
|
return getDefaultSiteConfigs();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await siteConfigs.json();
|
const data = await siteConfigs.json();
|
||||||
|
span.setAttribute("configs.count", data.length);
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: { operation: 'getSiteConfigs' }
|
||||||
|
});
|
||||||
console.warn('Failed to fetch site configs, using defaults:', error);
|
console.warn('Failed to fetch site configs, using defaults:', error);
|
||||||
return getDefaultSiteConfigs();
|
return getDefaultSiteConfigs();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultSiteConfigs() {
|
function getDefaultSiteConfigs() {
|
||||||
|
|||||||
66
lib/logger.ts
Normal file
66
lib/logger.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
// 获取Sentry logger实例
|
||||||
|
const { logger } = Sentry;
|
||||||
|
|
||||||
|
// 导出logger以便在其他文件中使用
|
||||||
|
export { logger };
|
||||||
|
|
||||||
|
// 示例用法的辅助函数
|
||||||
|
export const logUserAction = (action: string, userId?: string, metadata?: Record<string, any>) => {
|
||||||
|
logger.info(logger.fmt`User action: ${action}`, {
|
||||||
|
userId,
|
||||||
|
action,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...metadata
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logPerformanceMetric = (metric: string, value: number, unit: string) => {
|
||||||
|
logger.debug(logger.fmt`Performance metric: ${metric} = ${value}${unit}`, {
|
||||||
|
metric,
|
||||||
|
value,
|
||||||
|
unit,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logApiCall = (endpoint: string, method: string, status: number, duration?: number) => {
|
||||||
|
const level = status >= 400 ? 'error' : status >= 300 ? 'warn' : 'info';
|
||||||
|
|
||||||
|
logger[level](logger.fmt`API ${method} ${endpoint} - ${status}`, {
|
||||||
|
endpoint,
|
||||||
|
method,
|
||||||
|
status,
|
||||||
|
duration,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logError = (error: Error, context?: Record<string, any>) => {
|
||||||
|
logger.error(logger.fmt`Error occurred: ${error.message}`, {
|
||||||
|
error: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
...context
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// WebGL相关的特殊logger
|
||||||
|
export const logWebGLError = (operation: string, error: string, context?: Record<string, any>) => {
|
||||||
|
logger.error(logger.fmt`WebGL error during ${operation}: ${error}`, {
|
||||||
|
operation,
|
||||||
|
error,
|
||||||
|
webglContext: true,
|
||||||
|
...context
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map相关的logger
|
||||||
|
export const logMapEvent = (event: string, details?: Record<string, any>) => {
|
||||||
|
logger.debug(logger.fmt`Map event: ${event}`, {
|
||||||
|
event,
|
||||||
|
component: 'map',
|
||||||
|
...details
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import {withSentryConfig} from "@sentry/nextjs";
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
@ -13,4 +14,35 @@ const nextConfig: NextConfig = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default withSentryConfig(nextConfig, {
|
||||||
|
// For all available options, see:
|
||||||
|
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
|
||||||
|
|
||||||
|
org: "lix-nr",
|
||||||
|
|
||||||
|
project: "lidar",
|
||||||
|
|
||||||
|
// Only print logs for uploading source maps in CI
|
||||||
|
silent: !process.env.CI,
|
||||||
|
|
||||||
|
// For all available options, see:
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||||
|
|
||||||
|
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||||
|
widenClientFileUpload: true,
|
||||||
|
|
||||||
|
// Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||||
|
// This can increase your server load as well as your hosting bill.
|
||||||
|
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||||
|
// side errors will fail.
|
||||||
|
// tunnelRoute: "/monitoring",
|
||||||
|
|
||||||
|
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||||
|
disableLogger: true,
|
||||||
|
|
||||||
|
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||||
|
// See the following for more information:
|
||||||
|
// https://docs.sentry.io/product/crons/
|
||||||
|
// https://vercel.com/docs/cron-jobs
|
||||||
|
automaticVercelMonitors: true
|
||||||
|
});
|
||||||
3935
package-lock.json
generated
3935
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -37,6 +37,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
"@sentry/nextjs": "^10.5.0",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "^3.34.1",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@tiptap/extension-highlight": "^3.1.0",
|
"@tiptap/extension-highlight": "^3.1.0",
|
||||||
|
|||||||
24
sentry.edge.config.ts
Normal file
24
sentry.edge.config.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
|
||||||
|
// The config you add here will be used whenever one of the edge features is loaded.
|
||||||
|
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://109bcfcc2d1cdd643e0af61409016900@o4505647824109568.ingest.us.sentry.io/4509868655181824",
|
||||||
|
|
||||||
|
integrations: [
|
||||||
|
// send console.log, console.warn, and console.error calls as logs to Sentry
|
||||||
|
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||||
|
tracesSampleRate: 1,
|
||||||
|
|
||||||
|
// Enable logs to be sent to Sentry
|
||||||
|
enableLogs: true,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
23
sentry.server.config.ts
Normal file
23
sentry.server.config.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// This file configures the initialization of Sentry on the server.
|
||||||
|
// The config you add here will be used whenever the server handles a request.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://109bcfcc2d1cdd643e0af61409016900@o4505647824109568.ingest.us.sentry.io/4509868655181824",
|
||||||
|
|
||||||
|
integrations: [
|
||||||
|
// send console.log, console.warn, and console.error calls as logs to Sentry
|
||||||
|
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||||
|
tracesSampleRate: 1,
|
||||||
|
|
||||||
|
// Enable logs to be sent to Sentry
|
||||||
|
enableLogs: true,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user