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
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
||||
132
CLAUDE.md
132
CLAUDE.md
@ -90,4 +90,134 @@ This is a Next.js 15 application built with React 19 that creates an interactive
|
||||
- `types/` - TypeScript type definitions
|
||||
- `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 vertexSource from '@/app/glsl/radar/verx.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 {
|
||||
style?: string
|
||||
@ -66,6 +68,15 @@ export function MapComponent({
|
||||
useEffect(() => {
|
||||
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({
|
||||
container: mapContainer.current,
|
||||
style: style,
|
||||
@ -81,6 +92,17 @@ export function MapComponent({
|
||||
|
||||
|
||||
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({
|
||||
type: 'globe'
|
||||
})
|
||||
@ -103,13 +125,24 @@ export function MapComponent({
|
||||
// Helper function to compile shader
|
||||
const compileShader = (source: string, type: number): WebGLShader | null => {
|
||||
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.compileShader(shader);
|
||||
|
||||
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);
|
||||
return null;
|
||||
}
|
||||
@ -131,7 +164,13 @@ export function MapComponent({
|
||||
gl.linkProgram(program);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -144,7 +183,11 @@ export function MapComponent({
|
||||
const tex = gl.createTexture()
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -242,7 +285,10 @@ export function MapComponent({
|
||||
// 只在缩放级别变化时更新网格数据
|
||||
const currentZoom = Math.floor(map.getZoom());
|
||||
if (currentZoom !== this.lastZoom) {
|
||||
console.log(`缩放级别变化: ${this.lastZoom} -> ${currentZoom}`);
|
||||
logMapEvent('zoom_change', {
|
||||
previousZoom: this.lastZoom,
|
||||
currentZoom: currentZoom
|
||||
});
|
||||
|
||||
// 智能计算最佳细分数量
|
||||
const performanceLevel = detectPerformanceLevel();
|
||||
@ -254,9 +300,22 @@ export function MapComponent({
|
||||
|
||||
// 获取细分建议信息
|
||||
const recommendation = getSubdivisionRecommendation(currentZoom, performanceLevel);
|
||||
console.log(`缩放级别: ${currentZoom}, 性能等级: ${performanceLevel}`);
|
||||
console.log(`细分建议: ${recommendation.subdivisions} (${recommendation.description})`);
|
||||
console.log(`三角形数量: ${recommendation.triangleCount}, 预计内存: ${recommendation.estimatedMemoryMB}MB`);
|
||||
|
||||
logger.debug(logger.fmt`Zoom level: ${currentZoom}, Performance level: ${performanceLevel}`, {
|
||||
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);
|
||||
|
||||
@ -282,7 +341,10 @@ export function MapComponent({
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
@ -302,7 +364,10 @@ export function MapComponent({
|
||||
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) {
|
||||
@ -415,6 +480,9 @@ export function MapComponent({
|
||||
if (map) {
|
||||
map.remove();
|
||||
}
|
||||
|
||||
// 结束Sentry span
|
||||
span.end();
|
||||
}
|
||||
|
||||
}, [mapContainer])
|
||||
@ -481,16 +549,27 @@ export function MapComponent({
|
||||
|
||||
// 拖动事件处理函数
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(true)
|
||||
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()
|
||||
setIsDragging(true)
|
||||
|
||||
// 记录拖动开始时的鼠标位置和colorbar位置
|
||||
dragRef.current = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
startPositionX: colorbarPosition.x,
|
||||
startPositionY: colorbarPosition.y
|
||||
}
|
||||
// 记录拖动开始时的鼠标位置和colorbar位置
|
||||
dragRef.current = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
startPositionX: colorbarPosition.x,
|
||||
startPositionY: colorbarPosition.y
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 全局鼠标事件监听
|
||||
|
||||
@ -97,10 +97,8 @@ export default function TableOfContents({ className }: TableOfContentsProps) {
|
||||
|
||||
// 点击目录项滚动到对应位置
|
||||
const scrollToHeading = useCallback((id: string) => {
|
||||
console.log('Scrolling to:', id); // 调试日志
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
console.log('Element found:', element); // 调试日志
|
||||
const offsetTop = element.getBoundingClientRect().top + window.pageYOffset - 100;
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
@ -110,7 +108,7 @@ export default function TableOfContents({ className }: TableOfContentsProps) {
|
||||
// 更新活跃状态
|
||||
setActiveId(id);
|
||||
} 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;
|
||||
147
lib/fetchers.ts
147
lib/fetchers.ts
@ -1,6 +1,7 @@
|
||||
import { gql, GraphQLClient } from "graphql-request";
|
||||
import type { PageData } from "@/types/page";
|
||||
import { getBaseUrl } from "./gr-client";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
|
||||
const PageQuery = gql/* GraphQL */ `
|
||||
@ -27,63 +28,113 @@ const BlockQuery = gql/* GraphQL */ `
|
||||
|
||||
|
||||
export async function fetchPage(slug: string, jwt?: string): Promise<PageData | null> {
|
||||
const client = new GraphQLClient(getBaseUrl());
|
||||
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());
|
||||
|
||||
if (jwt) {
|
||||
client.setHeader('Authorization', `Bearer ${jwt}`);
|
||||
}
|
||||
if (jwt) {
|
||||
client.setHeader('Authorization', `Bearer ${jwt}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取页面基本信息
|
||||
const pageResponse: any = await client.request(PageQuery, { slug });
|
||||
try {
|
||||
// 获取页面基本信息
|
||||
const pageResponse: any = await Sentry.startSpan(
|
||||
{
|
||||
op: "http.client",
|
||||
name: "GraphQL PageQuery",
|
||||
},
|
||||
() => client.request(PageQuery, { slug })
|
||||
);
|
||||
|
||||
if (!pageResponse?.pageBySlug) {
|
||||
throw new Error('Page not found');
|
||||
if (!pageResponse?.pageBySlug) {
|
||||
throw new Error('Page not found');
|
||||
}
|
||||
|
||||
// 获取页面块数据
|
||||
const blocksResponse: any = await Sentry.startSpan(
|
||||
{
|
||||
op: "http.client",
|
||||
name: "GraphQL BlockQuery",
|
||||
},
|
||||
() => client.request(BlockQuery, {
|
||||
pageId: pageResponse.pageBySlug.id
|
||||
})
|
||||
);
|
||||
|
||||
if (!blocksResponse?.pageBlocks) {
|
||||
throw new Error('Failed to fetch page blocks');
|
||||
}
|
||||
|
||||
span.setAttribute("page.blocksCount", blocksResponse.pageBlocks.length);
|
||||
|
||||
// 合并数据
|
||||
return {
|
||||
...pageResponse.pageBySlug,
|
||||
blocks: blocksResponse.pageBlocks,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
Sentry.captureException(error, {
|
||||
tags: { operation: 'fetchPage' },
|
||||
extra: { slug, hasJwt: !!jwt }
|
||||
});
|
||||
console.error('Failed to fetch page:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取页面块数据
|
||||
const blocksResponse: any = await client.request(BlockQuery, {
|
||||
pageId: pageResponse.pageBySlug.id
|
||||
});
|
||||
|
||||
if (!blocksResponse?.pageBlocks) {
|
||||
throw new Error('Failed to fetch page blocks');
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
return {
|
||||
...pageResponse.pageBySlug,
|
||||
blocks: blocksResponse.pageBlocks,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch page:', error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getSiteConfigs() {
|
||||
try {
|
||||
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
|
||||
const siteConfigs = await fetch(`${baseUrl}/api/site`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
// Add timeout to prevent hanging during build
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
return Sentry.startSpan(
|
||||
{
|
||||
op: "http.client",
|
||||
name: "Fetch Site Configs",
|
||||
},
|
||||
async (span) => {
|
||||
try {
|
||||
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
|
||||
span.setAttribute("api.baseUrl", baseUrl);
|
||||
|
||||
const siteConfigs = await fetch(`${baseUrl}/api/site`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
// Add timeout to prevent hanging during build
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!siteConfigs.ok) {
|
||||
console.warn(`Failed to fetch site configs: ${siteConfigs.status} ${siteConfigs.statusText}`);
|
||||
return getDefaultSiteConfigs();
|
||||
span.setAttribute("http.status_code", siteConfigs.status);
|
||||
|
||||
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}`);
|
||||
return getDefaultSiteConfigs();
|
||||
}
|
||||
|
||||
const data = await siteConfigs.json();
|
||||
span.setAttribute("configs.count", data.length);
|
||||
return data;
|
||||
} catch (error) {
|
||||
Sentry.captureException(error, {
|
||||
tags: { operation: 'getSiteConfigs' }
|
||||
});
|
||||
console.warn('Failed to fetch site configs, using defaults:', error);
|
||||
return getDefaultSiteConfigs();
|
||||
}
|
||||
}
|
||||
|
||||
const data = await siteConfigs.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch site configs, using defaults:', error);
|
||||
return 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";
|
||||
|
||||
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-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@sentry/nextjs": "^10.5.0",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@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