add sentry

This commit is contained in:
tsuki 2025-08-19 12:17:11 +08:00
parent 7ba2926da0
commit 9dcc23a180
15 changed files with 4245 additions and 325 deletions

3
.gitignore vendored
View File

@ -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
View File

@ -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,
});
```

View 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
View 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>
);
}

View File

@ -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,16 +549,27 @@ export function MapComponent({
// 拖动事件处理函数 // 拖动事件处理函数
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault() Sentry.startSpan(
setIsDragging(true) {
op: "ui.interaction",
name: "Colorbar Drag Start",
},
(span) => {
span.setAttribute("colorbar.position.x", colorbarPosition.x);
span.setAttribute("colorbar.position.y", colorbarPosition.y);
// 记录拖动开始时的鼠标位置和colorbar位置 e.preventDefault()
dragRef.current = { setIsDragging(true)
startX: e.clientX,
startY: e.clientY, // 记录拖动开始时的鼠标位置和colorbar位置
startPositionX: colorbarPosition.x, dragRef.current = {
startPositionY: colorbarPosition.y startX: e.clientX,
} startY: e.clientY,
startPositionX: colorbarPosition.x,
startPositionY: colorbarPosition.y
}
}
);
} }
// 全局鼠标事件监听 // 全局鼠标事件监听

View File

@ -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
View 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
View 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;

View File

@ -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,63 +28,113 @@ 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> {
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);
if (jwt) { const client = new GraphQLClient(getBaseUrl());
client.setHeader('Authorization', `Bearer ${jwt}`);
}
try { if (jwt) {
// 获取页面基本信息 client.setHeader('Authorization', `Bearer ${jwt}`);
const pageResponse: any = await client.request(PageQuery, { slug }); }
if (!pageResponse?.pageBySlug) { try {
throw new Error('Page not found'); // 获取页面基本信息
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');
}
// 获取页面块数据
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() { export async function getSiteConfigs() {
try { return Sentry.startSpan(
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; {
const siteConfigs = await fetch(`${baseUrl}/api/site`, { op: "http.client",
headers: { name: "Fetch Site Configs",
'Content-Type': 'application/json', },
}, async (span) => {
// Add timeout to prevent hanging during build try {
signal: AbortSignal.timeout(5000) const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
}); span.setAttribute("api.baseUrl", baseUrl);
if (!siteConfigs.ok) { const siteConfigs = await fetch(`${baseUrl}/api/site`, {
console.warn(`Failed to fetch site configs: ${siteConfigs.status} ${siteConfigs.statusText}`); headers: {
return getDefaultSiteConfigs(); 'Content-Type': 'application/json',
},
// Add timeout to prevent hanging during build
signal: AbortSignal.timeout(5000)
});
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() { function getDefaultSiteConfigs() {

66
lib/logger.ts Normal file
View 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
});
};

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
View 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,
});