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:
915
package-lock.json
generated
915
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
|
"@sentry/node": "^10.39.0",
|
||||||
"dotenv": "^17.2.3"
|
"dotenv": "^17.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
127
src/index.ts
127
src/index.ts
@@ -18,6 +18,9 @@ const __dirname = dirname(__filename);
|
|||||||
const envPath = join(__dirname, '..', '.env');
|
const envPath = join(__dirname, '..', '.env');
|
||||||
dotenv.config({ path: envPath, override: true });
|
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 { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
import {
|
import {
|
||||||
@@ -50,78 +53,80 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
const { name, arguments: args } = request.params;
|
const { name, arguments: args } = request.params;
|
||||||
const a = args as any;
|
const a = args as any;
|
||||||
|
|
||||||
let result: string;
|
return withSentryTransaction(name, async () => {
|
||||||
|
let result: string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'confluence_list_spaces':
|
case 'confluence_list_spaces':
|
||||||
result = await handleListSpaces({ limit: a.limit });
|
result = await handleListSpaces({ limit: a.limit });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'confluence_create_space':
|
case 'confluence_create_space':
|
||||||
result = await handleCreateSpace({
|
result = await handleCreateSpace({
|
||||||
key: a.key,
|
key: a.key,
|
||||||
name: a.name,
|
name: a.name,
|
||||||
description: a.description,
|
description: a.description,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'confluence_search':
|
case 'confluence_search':
|
||||||
result = await handleSearch({
|
result = await handleSearch({
|
||||||
query: a.query,
|
query: a.query,
|
||||||
space_id: a.space_id,
|
space_id: a.space_id,
|
||||||
limit: a.limit,
|
limit: a.limit,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'confluence_get_page':
|
case 'confluence_get_page':
|
||||||
result = await handleGetPage({ page_id: String(a.page_id) });
|
result = await handleGetPage({ page_id: String(a.page_id) });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'confluence_create_page':
|
case 'confluence_create_page':
|
||||||
result = await handleCreatePage({
|
result = await handleCreatePage({
|
||||||
space_id: a.space_id,
|
space_id: a.space_id,
|
||||||
title: a.title,
|
title: a.title,
|
||||||
body: a.body,
|
body: a.body,
|
||||||
parent_id: a.parent_id,
|
parent_id: a.parent_id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'confluence_update_page':
|
case 'confluence_update_page':
|
||||||
result = await handleUpdatePage({
|
result = await handleUpdatePage({
|
||||||
page_id: String(a.page_id),
|
page_id: String(a.page_id),
|
||||||
title: a.title,
|
title: a.title,
|
||||||
body: a.body,
|
body: a.body,
|
||||||
version_number: a.version_number,
|
version_number: a.version_number,
|
||||||
version_message: a.version_message,
|
version_message: a.version_message,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'confluence_get_comments':
|
case 'confluence_get_comments':
|
||||||
result = await handleGetComments({
|
result = await handleGetComments({
|
||||||
page_id: String(a.page_id),
|
page_id: String(a.page_id),
|
||||||
limit: a.limit,
|
limit: a.limit,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'confluence_add_comment':
|
case 'confluence_add_comment':
|
||||||
result = await handleAddComment({
|
result = await handleAddComment({
|
||||||
page_id: String(a.page_id),
|
page_id: String(a.page_id),
|
||||||
body: a.body,
|
body: a.body,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
result = `Unknown tool: ${name}`;
|
result = `Unknown tool: ${name}`;
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
result = `Error: ${message}`;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
result = `Error: ${message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: result }],
|
content: [{ type: 'text', text: result }],
|
||||||
};
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
|||||||
86
src/sentry.ts
Normal file
86
src/sentry.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user