feat(CF-536): Integrate comprehensive Sentry into task-mcp
- Create sentry.ts with MCP-aware initialization and PII scrubbing - Replace basic inline Sentry initialization with initSentry() - Update .env.vault-mapping for sentry.backend-node.dsn secret - Includes PostgreSQL integration and transaction tracing Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,10 @@
|
|||||||
#
|
#
|
||||||
# Generated for task-mcp OpenBao integration
|
# Generated for task-mcp OpenBao integration
|
||||||
# Date: 2026-01-24
|
# Date: 2026-01-24
|
||||||
|
# Updated: 2026-01-29 for Sentry integration (CF-536)
|
||||||
|
|
||||||
|
# Sentry error tracking DSN
|
||||||
|
kv:kv:secret/sentry/backend-node:dsn=SENTRY_DSN
|
||||||
|
|
||||||
# LLM API Key (for embeddings)
|
# LLM API Key (for embeddings)
|
||||||
kv:kv:litellm/master_key:value=LLM_API_KEY
|
kv:kv:litellm/master_key:value=LLM_API_KEY
|
||||||
|
|||||||
13
src/index.ts
13
src/index.ts
@@ -13,22 +13,15 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
const envPath = join(__dirname, '..', '.env');
|
const envPath = join(__dirname, '..', '.env');
|
||||||
const result = dotenv.config({ path: envPath, override: true });
|
const result = dotenv.config({ path: envPath, override: true });
|
||||||
|
|
||||||
// Initialize Sentry for error tracking
|
// Initialize Sentry for error tracking (with MCP-aware filtering and PII scrubbing)
|
||||||
if (process.env.SENTRY_DSN) {
|
import { initSentry } from './sentry.js';
|
||||||
Sentry.init({
|
initSentry(process.env.SENTRY_ENVIRONMENT || 'production');
|
||||||
dsn: process.env.SENTRY_DSN,
|
|
||||||
environment: process.env.SENTRY_ENVIRONMENT || 'production',
|
|
||||||
tracesSampleRate: parseFloat(process.env.SENTRY_API_TRACE_RATE || '0.1'),
|
|
||||||
integrations: [Sentry.postgresIntegration()],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log environment loading status (goes to MCP server logs)
|
// Log environment loading status (goes to MCP server logs)
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
|
|||||||
115
src/sentry.ts
Normal file
115
src/sentry.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* Sentry SDK integration for MCP servers.
|
||||||
|
*
|
||||||
|
* Provides error tracking and performance monitoring for MCP protocol tools
|
||||||
|
* while filtering out normal error responses (isError: true).
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { initSentry } from "./sentry.js";
|
||||||
|
*
|
||||||
|
* initSentry(process.env.ENVIRONMENT || "development");
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
import { nodeProfilingIntegration } from "@sentry/profiling-node";
|
||||||
|
|
||||||
|
export function initSentry(environment: string = "development"): void {
|
||||||
|
const dsn = process.env.SENTRY_DSN || "";
|
||||||
|
|
||||||
|
if (!dsn) {
|
||||||
|
console.error("SENTRY_DSN not set, Sentry disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn,
|
||||||
|
environment,
|
||||||
|
tracesSampleRate: parseFloat(process.env.SENTRY_TRACE_SAMPLE_RATE || "0.1"),
|
||||||
|
profilesSampleRate: parseFloat(
|
||||||
|
process.env.SENTRY_PROFILE_SAMPLE_RATE || "0.01"
|
||||||
|
),
|
||||||
|
integrations: [
|
||||||
|
nodeProfilingIntegration(),
|
||||||
|
new Sentry.Integrations.Http({ tracing: true }),
|
||||||
|
new Sentry.Integrations.Postgres({ recordStatementAsSpans: true }),
|
||||||
|
],
|
||||||
|
beforeSend(event, hint) {
|
||||||
|
// MCP protocol: Don't send normal error responses (isError: true)
|
||||||
|
const originalException = hint.originalException as any;
|
||||||
|
if (originalException?.isError === true) {
|
||||||
|
return null; // This is a normal MCP error response, not an exception
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip PII from headers
|
||||||
|
if (event.request?.headers) {
|
||||||
|
const headers = { ...event.request.headers };
|
||||||
|
delete headers.Authorization;
|
||||||
|
delete headers.Cookie;
|
||||||
|
delete headers["X-API-Key"];
|
||||||
|
event.request.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redact sensitive data in request body
|
||||||
|
if (event.request?.data) {
|
||||||
|
let dataStr = JSON.stringify(event.request.data);
|
||||||
|
// Redact API keys
|
||||||
|
dataStr = dataStr.replace(
|
||||||
|
/(api[_-]?key|token|secret)["']?\s*[:=]\s*["']?[\w-]+/gi,
|
||||||
|
"$1=REDACTED"
|
||||||
|
);
|
||||||
|
// Redact emails
|
||||||
|
dataStr = dataStr.replace(
|
||||||
|
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
||||||
|
"EMAIL_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, // Never send PII automatically
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`✅ Sentry initialized: ${environment} - ${process.env.APP_VERSION || "unknown"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap MCP tool handler with Sentry transaction tracking.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* const result = await withSentryTransaction("tool_name", async () => {
|
||||||
|
* return await performToolOperation();
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export async function withSentryTransaction<T>(
|
||||||
|
toolName: string,
|
||||||
|
handler: () => Promise<T>
|
||||||
|
): Promise<T> {
|
||||||
|
return Sentry.startActiveSpan(
|
||||||
|
{ name: `tool_${toolName}`, op: "mcp.tool" },
|
||||||
|
async (span) => {
|
||||||
|
try {
|
||||||
|
const result = await handler();
|
||||||
|
span.setStatus("ok");
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
// Capture exception to Sentry (unless it's a normal MCP error)
|
||||||
|
if (!error.isError) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
}
|
||||||
|
span.recordException(error);
|
||||||
|
span.setStatus("error");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user