feat(CF-1356): Add Sentry error tracking

Add sentry.ts with initSentry + withSentryTransaction, wrap all
tool call handlers with transaction tracing and error capture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-02-18 12:20:49 +02:00
parent ed2d16f845
commit 3f7528a317
4 changed files with 1066 additions and 63 deletions

915
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"@sentry/node": "^10.39.0",
"dotenv": "^17.2.3"
},
"devDependencies": {

View File

@@ -18,6 +18,9 @@ const __dirname = dirname(__filename);
const envPath = join(__dirname, '..', '.env');
dotenv.config({ path: envPath, override: true });
import { initSentry, withSentryTransaction } from './sentry.js';
initSentry(process.env.SENTRY_ENVIRONMENT || 'production');
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
@@ -50,6 +53,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const a = args as any;
return withSentryTransaction(name, async () => {
let result: string;
try {
@@ -123,6 +127,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
content: [{ type: 'text', text: result }],
};
});
});
async function main() {
const transport = new StdioServerTransport();

86
src/sentry.ts Normal file
View File

@@ -0,0 +1,86 @@
/**
* Sentry SDK integration for confluence-mcp.
*
* Provides error tracking and performance monitoring for MCP tool calls
* while filtering out normal error responses (isError: true).
*/
import * as Sentry from "@sentry/node";
export function initSentry(environment: string = "development"): void {
const dsn = process.env.SENTRY_DSN || "";
if (!dsn) {
console.error("[sentry] SENTRY_DSN not set, Sentry disabled");
return;
}
Sentry.init({
dsn,
environment,
tracesSampleRate: parseFloat(process.env.SENTRY_TRACE_SAMPLE_RATE || "0.1"),
integrations: [
Sentry.httpIntegration(),
],
beforeSend(event: Sentry.ErrorEvent, hint: Sentry.EventHint) {
const originalException = hint.originalException as any;
if (originalException?.isError === true) {
return null;
}
if (event.request?.headers) {
const headers = { ...event.request.headers };
delete headers.Authorization;
delete headers.Cookie;
delete headers["X-API-Key"];
event.request.headers = headers;
}
if (event.request?.data) {
let dataStr = JSON.stringify(event.request.data);
dataStr = dataStr.replace(
/(api[_-]?key|token|secret)["']?\s*[:=]\s*["']?[\w-]+/gi,
"$1=REDACTED"
);
try {
event.request.data = JSON.parse(dataStr);
} catch {
event.request.data = dataStr;
}
}
return event;
},
maxBreadcrumbs: 30,
attachStacktrace: true,
release: process.env.APP_VERSION || "unknown",
sendDefaultPii: false,
});
console.error(
`[sentry] Initialized: ${environment} - ${process.env.APP_VERSION || "unknown"}`
);
}
export async function withSentryTransaction<T>(
toolName: string,
handler: () => Promise<T>
): Promise<T> {
return Sentry.startSpan(
{ name: `tool_${toolName}`, op: "mcp.tool" },
async (span: Sentry.Span) => {
try {
const result = await handler();
span.setStatus({ code: 1, message: "ok" });
return result;
} catch (error: unknown) {
const err = error as { isError?: boolean };
if (!err.isError) {
Sentry.captureException(error);
}
span.setStatus({ code: 2, message: "error" });
throw error;
}
}
);
}