Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
160 lines
5.1 KiB
Plaintext
160 lines
5.1 KiB
Plaintext
import { SPAN_STATUS_ERROR } from '../../tracing/spanstatus.js';
|
|
import { MCP_PROTOCOL_VERSION_ATTRIBUTE } from './attributes.js';
|
|
import { extractToolResultAttributes, extractPromptResultAttributes } from './resultExtraction.js';
|
|
import { extractSessionDataFromInitializeResponse, buildServerAttributesFromInfo } from './sessionExtraction.js';
|
|
|
|
/**
|
|
* Request-span correlation system for MCP server instrumentation
|
|
*
|
|
* Handles mapping requestId to span data for correlation with handler execution.
|
|
*
|
|
* Uses sessionId as the primary key for stateful transports. This handles the wrapper
|
|
* transport pattern (e.g., NodeStreamableHTTPServerTransport wrapping WebStandardStreamableHTTPServerTransport)
|
|
* where onmessage and send may receive different `this` values but share the same sessionId.
|
|
*
|
|
* Falls back to WeakMap by transport instance for stateless transports (no sessionId).
|
|
*/
|
|
|
|
|
|
/**
|
|
* Session-scoped correlation for stateful transports (with sessionId)
|
|
* @internal Using sessionId as key handles wrapper transport patterns where
|
|
* different transport objects share the same logical session
|
|
*/
|
|
const sessionToSpanMap = new Map();
|
|
|
|
/**
|
|
* Transport-scoped correlation fallback for stateless transports (no sessionId)
|
|
* @internal WeakMap allows automatic cleanup when transport is garbage collected
|
|
*/
|
|
const statelessSpanMap = new WeakMap();
|
|
|
|
/**
|
|
* Gets or creates the span map for a transport, using sessionId when available
|
|
* @internal
|
|
* @param transport - MCP transport instance
|
|
* @returns Span map for the transport/session
|
|
*/
|
|
function getOrCreateSpanMap(transport) {
|
|
const sessionId = transport.sessionId;
|
|
|
|
if (sessionId) {
|
|
// Stateful transport - use sessionId as key (handles wrapper pattern)
|
|
let spanMap = sessionToSpanMap.get(sessionId);
|
|
if (!spanMap) {
|
|
spanMap = new Map();
|
|
sessionToSpanMap.set(sessionId, spanMap);
|
|
}
|
|
return spanMap;
|
|
}
|
|
|
|
// Stateless fallback - use transport instance as key
|
|
let spanMap = statelessSpanMap.get(transport);
|
|
if (!spanMap) {
|
|
spanMap = new Map();
|
|
statelessSpanMap.set(transport, spanMap);
|
|
}
|
|
return spanMap;
|
|
}
|
|
|
|
/**
|
|
* Stores span context for later correlation with handler execution
|
|
* @param transport - MCP transport instance
|
|
* @param requestId - Request identifier
|
|
* @param span - Active span to correlate
|
|
* @param method - MCP method name
|
|
*/
|
|
function storeSpanForRequest(transport, requestId, span, method) {
|
|
const spanMap = getOrCreateSpanMap(transport);
|
|
spanMap.set(requestId, {
|
|
span,
|
|
method,
|
|
// eslint-disable-next-line @sentry-internal/sdk/no-unsafe-random-apis
|
|
startTime: Date.now(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Completes span with results and cleans up correlation
|
|
* @param transport - MCP transport instance
|
|
* @param requestId - Request identifier
|
|
* @param result - Execution result for attribute extraction
|
|
* @param options - Resolved MCP options
|
|
*/
|
|
function completeSpanWithResults(
|
|
transport,
|
|
requestId,
|
|
result,
|
|
options,
|
|
) {
|
|
const spanMap = getOrCreateSpanMap(transport);
|
|
const spanData = spanMap.get(requestId);
|
|
if (spanData) {
|
|
const { span, method } = spanData;
|
|
|
|
if (method === 'initialize') {
|
|
const sessionData = extractSessionDataFromInitializeResponse(result);
|
|
const serverAttributes = buildServerAttributesFromInfo(sessionData.serverInfo);
|
|
|
|
const initAttributes = {
|
|
...serverAttributes,
|
|
};
|
|
if (sessionData.protocolVersion) {
|
|
initAttributes[MCP_PROTOCOL_VERSION_ATTRIBUTE] = sessionData.protocolVersion;
|
|
}
|
|
|
|
span.setAttributes(initAttributes);
|
|
} else if (method === 'tools/call') {
|
|
const toolAttributes = extractToolResultAttributes(result, options.recordOutputs);
|
|
span.setAttributes(toolAttributes);
|
|
} else if (method === 'prompts/get') {
|
|
const promptAttributes = extractPromptResultAttributes(result, options.recordOutputs);
|
|
span.setAttributes(promptAttributes);
|
|
}
|
|
|
|
span.end();
|
|
spanMap.delete(requestId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleans up pending spans for a specific transport (when that transport closes)
|
|
* @param transport - MCP transport instance
|
|
*/
|
|
function cleanupPendingSpansForTransport(transport) {
|
|
const sessionId = transport.sessionId;
|
|
|
|
// Try sessionId-based cleanup first (for stateful transports)
|
|
if (sessionId) {
|
|
const spanMap = sessionToSpanMap.get(sessionId);
|
|
if (spanMap) {
|
|
for (const [, spanData] of spanMap) {
|
|
spanData.span.setStatus({
|
|
code: SPAN_STATUS_ERROR,
|
|
message: 'cancelled',
|
|
});
|
|
spanData.span.end();
|
|
}
|
|
sessionToSpanMap.delete(sessionId);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Fallback to transport-based cleanup (for stateless transports)
|
|
const spanMap = statelessSpanMap.get(transport);
|
|
if (spanMap) {
|
|
for (const [, spanData] of spanMap) {
|
|
spanData.span.setStatus({
|
|
code: SPAN_STATUS_ERROR,
|
|
message: 'cancelled',
|
|
});
|
|
spanData.span.end();
|
|
}
|
|
spanMap.clear();
|
|
// Note: WeakMap entries are automatically cleaned up when transport is GC'd
|
|
}
|
|
}
|
|
|
|
export { cleanupPendingSpansForTransport, completeSpanWithResults, storeSpanForRequest };
|
|
//# sourceMappingURL=correlation.js.map
|