Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
ReplayListResponseSchema,
ReplayIdsByResourceSchema,
ReplayRecordingSegmentsSchema,
StacktraceLinkSchema,
AIConversationSummaryListSchema,
AIConversationSpanListSchema,
UserReportListSchema,
Expand Down Expand Up @@ -129,6 +130,7 @@ import type {
ReplayDetails,
ReplayList,
ReplayRecordingSegments,
StacktraceLink,
AIConversationSummary,
AIConversationSpanList,
UserReportList,
Expand Down Expand Up @@ -3308,6 +3310,58 @@ export class SentryApiService {
);
}

/**
* Resolves a stack frame through the project's configured source code mapping.
* The upstream endpoint verifies the mapped path with the SCM integration.
*/
async getStacktraceLink(
{
organizationSlug,
projectSlug,
file,
platform,
lineNo,
absPath,
module,
package: framePackage,
commitId,
groupId,
sdkName,
signal,
}: {
organizationSlug: string;
projectSlug: string;
file: string;
platform?: string;
lineNo?: number;
absPath?: string;
module?: string;
package?: string;
commitId?: string;
groupId?: string;
sdkName?: string;
signal?: AbortSignal;
},
opts?: RequestOptions,
): Promise<StacktraceLink> {
const query = new URLSearchParams({ file });
if (platform) query.set("platform", platform);
if (lineNo !== undefined) query.set("lineNo", String(lineNo));
if (absPath) query.set("absPath", absPath);
if (module) query.set("module", module);
if (framePackage) query.set("package", framePackage);
if (commitId) query.set("commitId", commitId);
if (groupId) query.set("groupId", groupId);
if (sdkName) query.set("sdkName", sdkName);

const body = await this.requestJSON(
`/projects/${organizationSlug}/${projectSlug}/stacktrace-link/?${query.toString()}`,
{ signal },
opts,
);
return StacktraceLinkSchema.parse(body);
}

/**
* Lists events for a specific issue.
* Uses the issue-specific endpoint which already filters by issue ID.
Expand Down
51 changes: 46 additions & 5 deletions packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -805,10 +805,13 @@ export const FrameInterface = z
colNo: z.number().nullable(),
absPath: z.string().nullable(),
module: z.string().nullable(),
package: z.string().nullable(),
platform: z.string().nullable(),
sourceLink: z.string().nullable(),
// lineno, source code
context: z.array(z.tuple([z.number(), z.string()])),
inApp: z.boolean().optional(),
vars: z.record(z.string(), z.unknown()).optional(),
vars: z.record(z.string(), z.unknown()).nullable().optional(),
})
.partial();

Expand All @@ -821,12 +824,15 @@ export const ExceptionInterface = z
type: z.string().nullable(),
handled: z.boolean().nullable(),
})
.partial(),
.partial()
.nullable(),
type: z.string().nullable(),
value: z.string().nullable(),
stacktrace: z.object({
frames: z.array(FrameInterface),
}),
stacktrace: z
.object({
frames: z.array(FrameInterface),
})
.nullable(),
})
.partial();

Expand Down Expand Up @@ -933,6 +939,7 @@ const EventTagsSchema = z.preprocess((value) => {

const BaseEventSchema = z.object({
id: z.string(),
groupID: z.string().nullable().optional(),
title: z.string(),
message: z.string().nullable(),
platform: z.string().nullable().optional(),
Expand Down Expand Up @@ -988,6 +995,26 @@ const BaseEventSchema = z.object({
// "context" (singular) is the legacy "extra" field for arbitrary user-defined data
// This is different from "contexts" (plural) which are structured contexts
context: z.record(z.string(), z.unknown()).optional(),
sdk: z
.object({
name: z.string().nullable().optional(),
})
.passthrough()
.nullable()
.optional(),
release: z
.object({
lastCommit: z
.object({
id: z.string(),
})
.passthrough()
.nullable()
.optional(),
})
.passthrough()
.nullable()
.optional(),
tags: EventTagsSchema.optional(),
user: z
.object({
Expand Down Expand Up @@ -1122,6 +1149,20 @@ export const EventSchema = z.union([
UnknownEventSchema,
]);

export const StacktraceLinkSchema = z
.object({
config: z
.object({
repoName: z.string(),
})
.passthrough()
.nullable()
.optional(),
sourcePath: z.string().nullable().optional(),
sourceUrl: z.string().nullable().optional(),
})
.passthrough();

export const EventsResponseSchema = z.object({
data: z.array(z.unknown()),
meta: z
Expand Down
2 changes: 2 additions & 0 deletions packages/mcp-core/src/api-client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import type {
ReplayListResponseSchema,
ReplayRecordingEventSchema,
ReplayRecordingSegmentsSchema,
StacktraceLinkSchema,
TagListSchema,
TagSchema,
TeamListSchema,
Expand Down Expand Up @@ -155,6 +156,7 @@ export type Event =
| TransactionEvent
| GenericEvent
| UnknownEvent;
export type StacktraceLink = z.infer<typeof StacktraceLinkSchema>;

export type EventAttachment = z.infer<typeof EventAttachmentSchema>;
export type Tag = z.infer<typeof TagSchema>;
Expand Down
32 changes: 32 additions & 0 deletions packages/mcp-core/src/internal/code-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { z } from "zod";
import type { FrameInterface } from "../api-client";

export type Frame = z.infer<typeof FrameInterface>;

export type CodeLocation = {
repository?: string;
path?: string;
line?: number;
url: string;
};

/** Formats a verified source location for the issue-details response. */
export function formatCodeLocation(codeLocation: CodeLocation): string {
const output = ["## Code Location", ""];
if (codeLocation.repository) {
output.push(`**Repository**: ${codeLocation.repository}`);
}
if (codeLocation.path) {
output.push(`**Path**: ${codeLocation.path}`);
}
if (codeLocation.line !== undefined) {
output.push(`**Line**: ${codeLocation.line}`);
}
output.push(`**Source**: ${codeLocation.url}`, "");
return `${output.join("\n")}\n`;
}

/** Selects Sentry's most relevant application frame: the last in-app frame. */
export function findMostRelevantInAppFrame(frames: Frame[]): Frame | undefined {
return frames.findLast((frame) => frame.inApp === true);
}
71 changes: 28 additions & 43 deletions packages/mcp-core/src/internal/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,23 @@ import type {
TraceSpan,
} from "../api-client/types";
import { logIssue } from "../telem/logging";
import {
type CodeLocation,
findMostRelevantInAppFrame,
formatCodeLocation,
} from "./code-location";
import {
type AIConversationReference,
formatAIConversationActionInstructions,
} from "./tool-helpers/ai-conversation-actions";
import {
getAutofixArtifactSummaries,
getStatusDisplayName,
isTerminalStatus,
} from "./tool-helpers/seer";
import { formatToolCallInstruction } from "./tool-helpers/tool-call-formatting";
import {
formatAIConversationActionInstructions,
type AIConversationReference,
} from "./tool-helpers/ai-conversation-actions";
import { formatUserGeoSummary } from "./user-formatting";
import { isPlainObject } from "./type-guards";
import { formatUserGeoSummary } from "./user-formatting";

/**
* Convert Seer fixability score to actionability label.
Expand Down Expand Up @@ -416,12 +421,12 @@ function formatExceptionInterfaceOutput(

// Only show enhanced frame for the first (outermost) exception to avoid overwhelming output
if (index === 0) {
const firstInAppFrame = findFirstInAppFrame(frames);
const relevantFrame = findMostRelevantInAppFrame(frames);
if (
firstInAppFrame &&
(firstInAppFrame.context?.length || firstInAppFrame.vars)
relevantFrame &&
(relevantFrame.context?.length || relevantFrame.vars)
) {
parts.push(renderEnhancedFrame(firstInAppFrame, event));
parts.push(renderEnhancedFrame(relevantFrame, event));
parts.push("");
parts.push("**Full Stacktrace:**");
parts.push("────────────────");
Expand Down Expand Up @@ -590,13 +595,10 @@ function formatThreadsInterfaceOutput(

const frames = crashedThread.stacktrace.frames;

// Find and format the first in-app frame with enhanced view
const firstInAppFrame = findFirstInAppFrame(frames);
if (
firstInAppFrame &&
(firstInAppFrame.context?.length || firstInAppFrame.vars)
) {
parts.push(renderEnhancedFrame(firstInAppFrame, event));
// Find and format the most relevant in-app frame with enhanced view
const relevantFrame = findMostRelevantInAppFrame(frames);
if (relevantFrame && (relevantFrame.context?.length || relevantFrame.vars)) {
parts.push(renderEnhancedFrame(relevantFrame, event));
parts.push("");
parts.push("**Full Stacktrace:**");
parts.push("────────────────");
Expand Down Expand Up @@ -725,12 +727,9 @@ export function formatThreadStacktraceOutput({
parts.push("");
}

const firstInAppFrame = findFirstInAppFrame(frames);
if (
firstInAppFrame &&
(firstInAppFrame.context?.length || firstInAppFrame.vars)
) {
parts.push(renderEnhancedFrame(firstInAppFrame, event));
const relevantFrame = findMostRelevantInAppFrame(frames);
if (relevantFrame && (relevantFrame.context?.length || relevantFrame.vars)) {
parts.push(renderEnhancedFrame(relevantFrame, event));
parts.push("");
parts.push("**Full Stacktrace:**");
parts.push("────────────────");
Expand Down Expand Up @@ -886,27 +885,6 @@ function renderVariablesTable(vars: Record<string, unknown>): string {
return lines.join("\n");
}

/**
* Finds the first application frame (in_app) in a stack trace.
* Searches from the bottom of the stack (oldest frame) to find the first
* frame that belongs to the user's application code rather than libraries.
*
* @param frames - Array of stack frames, typically in reverse chronological order
* @returns The first in-app frame found, or undefined if none exist
*/
function findFirstInAppFrame(
frames: z.infer<typeof FrameInterface>[],
): z.infer<typeof FrameInterface> | undefined {
// Frames are usually in reverse order (most recent first)
// We want the first in-app frame from the bottom
for (let i = frames.length - 1; i >= 0; i--) {
if (frames[i].inApp === true) {
return frames[i];
}
}
return undefined;
}

/**
* Constants for performance issue formatting
*/
Expand Down Expand Up @@ -1979,6 +1957,7 @@ export function formatIssueOutput({
externalIssues,
relatedReplayIds,
aiConversations,
codeLocation,
experimentalMode,
availableToolNames,
directToolNames,
Expand All @@ -1992,6 +1971,7 @@ export function formatIssueOutput({
externalIssues?: ExternalIssueList;
relatedReplayIds?: string[];
aiConversations?: AIConversationReference[];
codeLocation?: CodeLocation;
experimentalMode?: boolean;
availableToolNames?: ReadonlySet<string>;
directToolNames?: ReadonlySet<string>;
Expand Down Expand Up @@ -2066,6 +2046,11 @@ export function formatIssueOutput({
output += `**Project**: ${issue.project.name}\n`;
output += `**URL**: ${apiService.getIssueUrl(organizationSlug, issue.shortId)}\n`;
output += "\n";

if (codeLocation) {
output += formatCodeLocation(codeLocation);
}

output += "## Event Details\n\n";

// Check if this is an unsupported event type
Expand Down
Loading
Loading