diff --git a/docs/contributing/tool-responses.md b/docs/contributing/tool-responses.md index 2805c164c..e99c0806b 100644 --- a/docs/contributing/tool-responses.md +++ b/docs/contributing/tool-responses.md @@ -125,6 +125,14 @@ omits the section. Commit lookup is optional: failures do not prevent issue details from loading, but unexpected server or response-validation failures are reported to Sentry. +For metric issues, `Metric Alert Details` comes from the occurrence's +`evidenceData`, which the event API returns camelCased +(`dataSources[].queryObj.snubaQuery`, `detectorId`). It shows the Metric +Monitor ID, dataset, aggregate, query, interval, environment, evaluated window, +value, and threshold. Response Notes replace the stacktrace and trace hints with +`get_metric_monitor_details` and a pre-filled `search_errors`, `search_logs`, +`search_traces`, or `search_metrics` call for the monitor's query. + The event API's legacy `context` field (extra data) can be `null`. Issue details must still load; omit the `Extra Data` section for null or empty values while preserving the event's error, stacktrace, and structured `contexts` data. diff --git a/packages/mcp-core/src/internal/formatting.test.ts b/packages/mcp-core/src/internal/formatting.test.ts index 04bba5d01..fc916b5ac 100644 --- a/packages/mcp-core/src/internal/formatting.test.ts +++ b/packages/mcp-core/src/internal/formatting.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { SentryApiService } from "../api-client"; import type { AutofixRunState, Event, Issue } from "../api-client/types"; import { @@ -2208,71 +2208,146 @@ describe("formatEventOutput", () => { }); }); -describe("formatEventOutput metric alert details", () => { - it("includes alert rule query context for metric_issue events", () => { - const event = { - id: "metric-event-1", - type: "generic", - title: "High error count", - message: null, - platform: "other", - dateCreated: "2026-06-01T00:00:00.000Z", - entries: [], - tags: [{ key: "level", value: "error" }], - occurrence: { - id: "occ-1", - projectId: 1, - eventId: "metric-event-1", - fingerprint: ["abc"], - issueTitle: "High error count", - subtitle: "Critical: Number of events in the last minute above 100", - type: 8001, - culprit: "", - evidenceData: { - value: 150, - alert_id: 42, - conditions: [{ type: "gt", comparison: 100 }], - data_sources: [ - { - query_obj: { - snuba_query: { - dataset: "events", - query: "process_shard OR OutboxFlushError", - aggregate: "count()", - time_window: 60, - environment: "production", - }, - }, - }, - ], +/** + * Mirrors a real metric issue event from `GET /issues/{id}/events/{id}/`: the + * event serializer camelCases the occurrence payload, including nested + * workflow-engine evidence and the serialized data source. + */ +function createMetricIssueEvent({ + evidenceData, + dateCreated = "2026-10-28T11:39:52.000Z", +}: { + evidenceData: Record; + dateCreated?: string; +}): Event { + return { + id: "4d6e1b607c564714a7de102a835fae54", + type: "generic", + title: "Error logs above 4000 over past 1 hour", + message: null, + platform: "python", + dateCreated, + entries: [], + tags: [ + { key: "interface_type", value: "contexts" }, + { key: "level", value: "error" }, + ], + contexts: { + trace: { + trace_id: "bfa06a9c589f4d4f89236378d2194adc", + span_id: "bfa06a9c589f4d4f", + status: "unknown", + type: "trace", + }, + }, + occurrence: { + id: "occ-1", + projectId: 1, + eventId: "4d6e1b607c564714a7de102a835fae54", + fingerprint: ["abc"], + issueTitle: "Error logs above 4000 over past 1 hour", + subtitle: "Critical: Number of error logs in the last hour above 4000", + type: 8001, + culprit: "", + evidenceData, + evidenceDisplay: [], + }, + } as unknown as Event; +} + +function createLogsMonitorEvidence( + snubaQuery: Record = {}, +): Record { + return { + value: 4988, + detectorId: 1234567, + dataPacketSourceId: "998877", + conditions: [{ id: 1, type: "gt", comparison: 4000, conditionResult: 75 }], + config: { detectionType: "static", thresholdPeriod: 1 }, + dataSources: [ + { + id: "55", + organizationId: "1", + type: "snuba_query_subscription", + sourceId: "998877", + queryObj: { + id: "998877", + status: 0, + subscription: "sub-1", + snubaQuery: { + id: "77", + dataset: "events_analytics_platform", + query: "severity:error", + aggregate: "count(message)", + timeWindow: 3600, + environment: "prod", + eventTypes: ["trace_item_log"], + extrapolationMode: "unknown", + ...snubaQuery, + }, }, - evidenceDisplay: [], }, - } as Event; + ], + }; +} - const output = formatEventOutput(event); +describe("formatEventOutput metric alert details", () => { + it("includes monitor query context from camelCased evidence", () => { + const output = formatEventOutput( + createMetricIssueEvent({ evidenceData: createLogsMonitorEvidence() }), + ); expect(output).toMatchInlineSnapshot(` "### Metric Alert Details - **Dataset**: events - **Aggregate**: count() - **Query**: \`process_shard OR OutboxFlushError\` - **Interval**: 60 second(s) - **Environment**: production - - **Evaluated Value**: 150 - **Threshold**: above 100 - **Alert Rule ID**: 42 + **Monitor ID**: 1234567 + **Dataset**: Logs + **Aggregate**: count(message) + **Query**: \`severity:error\` + **Interval**: 3600 second(s) + **Environment**: prod + **Evaluated Window**: 2026-10-28T10:39:52.000Z to 2026-10-28T11:39:52.000Z + **Evaluated Value**: 4988 + **Threshold**: above 4000 ### Tags + **interface_type**: contexts **level**: error + ### Additional Context + + These are additional context provided by the user when they're instrumenting their application. + + **trace** + trace_id: "bfa06a9c589f4d4f89236378d2194adc" + span_id: "bfa06a9c589f4d4f" + status: "unknown" + " `); }); + it("labels errors and spans datasets and keeps unknown datasets verbatim", () => { + const datasetLabel = (snubaQuery: Record) => + formatEventOutput( + createMetricIssueEvent({ + evidenceData: createLogsMonitorEvidence(snubaQuery), + }), + ).match(/\*\*Dataset\*\*: (.+)/)?.[1]; + + expect(datasetLabel({ dataset: "events", eventTypes: ["error"] })).toBe( + "Errors", + ); + expect(datasetLabel({ eventTypes: ["trace_item_span"] })).toBe("Spans"); + expect(datasetLabel({ eventTypes: ["trace_item_metric"] })).toBe( + "Application metrics", + ); + expect( + datasetLabel({ dataset: "transactions", eventTypes: ["transaction"] }), + ).toBe("transactions"); + }); + it("keeps performance regression formatting for regression evidence", () => { const event = { id: "regression-event-1", @@ -2312,123 +2387,157 @@ describe("formatEventOutput metric alert details", () => { expect(output).toContain("POST /oauth/token duration increased"); }); - it("formats below-threshold conditions using workflow-engine type strings", () => { - const event = { - id: "metric-event-2", - type: "generic", - title: "Low throughput", - message: null, - platform: "other", - dateCreated: "2026-06-01T00:00:00.000Z", - entries: [], - occurrence: { - id: "occ-3", - projectId: 1, - eventId: "metric-event-2", - fingerprint: ["ghi"], - issueTitle: "Low throughput", - type: 8001, - culprit: "", - evidenceData: { - value: 5, - alert_id: 99, - conditions: [{ type: "lt", comparison: 10 }], - data_sources: [ - { - query_obj: { - snuba_query: { - dataset: "events", - query: "", - aggregate: "count()", - time_window: 300, - }, - }, - }, - ], - }, - }, - } as Event; + it("formats below-threshold conditions and omits an empty environment", () => { + const evidenceData = createLogsMonitorEvidence({ environment: null }); + evidenceData.conditions = [{ type: "lt", comparison: 10 }]; - const output = formatEventOutput(event); + const output = formatEventOutput(createMetricIssueEvent({ evidenceData })); expect(output).toContain("**Threshold**: below 10"); expect(output).not.toContain("**Environment**:"); }); - it("supports alert_id-only metric alert evidence without data_sources", () => { - const event = { - id: "metric-event-3", - type: "generic", - title: "Alert triggered", - message: null, - platform: "other", - dateCreated: "2026-06-01T00:00:00.000Z", - entries: [], - occurrence: { - id: "occ-4", - projectId: 1, - eventId: "metric-event-3", - fingerprint: ["jkl"], - issueTitle: "Alert triggered", - type: 8001, - culprit: "", + it("renders value, threshold and legacy alert rule ID without data sources", () => { + const output = formatEventOutput( + createMetricIssueEvent({ evidenceData: { value: 42, - alert_id: 7, + alertId: 7, conditions: [ { type: "gte", comparison: 40 }, { type: "lte", comparison: 50 }, ], }, - }, - } as Event; - - const output = formatEventOutput(event); + }), + ); expect(output).toContain("**Evaluated Value**: 42"); expect(output).toContain("**Alert Rule ID**: 7"); expect(output).toContain("**Threshold**: at or above 40, at or below 50"); expect(output).not.toContain("**Dataset**:"); + expect(output).not.toContain("**Evaluated Window**:"); }); it("formats dynamic alert values without object placeholders", () => { - const event = { - id: "metric-event-4", - type: "generic", - title: "Dynamic throughput alert", - message: null, - platform: "other", - dateCreated: "2026-06-01T00:00:00.000Z", - entries: [], - occurrence: { - id: "occ-5", - projectId: 1, - eventId: "metric-event-4", - fingerprint: ["mno"], - issueTitle: "Dynamic throughput alert", - type: 8001, - culprit: "", - evidenceData: { - value: { value: 12.5, source_id: "123", timestamp: 1780272000 }, - alert_id: 11, - conditions: [ - { - type: "anomaly_detection", - comparison: { - sensitivity: "medium", - seasonality: "auto", - threshold_type: 0, - }, - }, - ], + const evidenceData = createLogsMonitorEvidence(); + evidenceData.value = { + value: 12.5, + source_id: "123", + timestamp: 1780272000, + }; + evidenceData.conditions = [ + { + type: "anomaly_detection", + comparison: { + sensitivity: "medium", + seasonality: "auto", + thresholdType: 0, }, }, - } as Event; + ]; - const output = formatEventOutput(event); + const output = formatEventOutput(createMetricIssueEvent({ evidenceData })); expect(output).toContain("**Evaluated Value**: 12.5"); expect(output).not.toContain("[object Object]"); expect(output).not.toContain("**Threshold**:"); }); }); + +describe("formatIssueOutput metric issue response notes", () => { + const metricIssue = { + id: "7001", + shortId: "SEER-A18", + title: "Error logs above 4000 over past 1 hour", + count: "2", + userCount: 0, + status: "resolved", + issueType: "metric_issue", + issueCategory: "metric", + project: { name: "seer", slug: "seer" }, + } as Issue; + const apiService = { + getIssueUrl: () => "https://sentry.sentry.io/issues/SEER-A18", + } as unknown as SentryApiService; + + function responseNotes(evidenceData: Record): string { + const output = formatIssueOutput({ + organizationSlug: "sentry", + issue: metricIssue, + event: createMetricIssueEvent({ evidenceData }), + apiService, + }); + return output.slice(output.indexOf("## Response Notes")); + } + + beforeEach(() => { + vi.useFakeTimers({ now: new Date("2026-10-29T02:30:00.000Z") }); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("points at the monitor and the data that breached it", () => { + expect(responseNotes(createLogsMonitorEvidence())).toMatchInlineSnapshot(` + "## Response Notes + + - Commit message issue reference: \`Fixes SEER-A18\` automatically closes the issue when the commit is merged. + - Metric Monitor configuration and connected Alerts: Use the Sentry tool \`get_metric_monitor_details(organizationSlug='sentry', monitorId='1234567')\` + - Logs matched by the monitor query: Use the Sentry tool \`search_logs(organizationSlug='sentry', projectSlug='seer', query='severity:error environment:prod', period='16h')\`, then focus on 2026-10-28T10:39:52.000Z to 2026-10-28T11:39:52.000Z + - Issue event search: Use the Sentry tool \`search_issue_events\` + " + `); + }); + + it("groups an OR monitor query before adding the environment filter", () => { + expect( + responseNotes( + createLogsMonitorEvidence({ + query: "severity:error OR severity:fatal", + }), + ), + ).toContain("query='(severity:error OR severity:fatal) environment:prod'"); + }); + + it("uses the dataset-specific search tool", () => { + expect( + responseNotes( + createLogsMonitorEvidence({ + dataset: "events", + query: "", + environment: "my env", + eventTypes: ["error", "default"], + }), + ), + ).toContain( + "Use the Sentry tool `search_errors(organizationSlug='sentry', projectSlug='seer', query='environment:\"my env\"', period='16h')`", + ); + expect( + responseNotes( + createLogsMonitorEvidence({ eventTypes: ["trace_item_span"] }), + ), + ).toContain( + "Spans matched by the monitor query: Use the Sentry tool `search_traces(", + ); + }); + + it("omits the search call when the dataset has no matching search tool", () => { + const notes = responseNotes( + createLogsMonitorEvidence({ + dataset: "metrics", + eventTypes: [], + }), + ); + + expect(notes).toContain("get_metric_monitor_details("); + expect(notes).not.toContain("matched by the monitor query"); + }); + + it("drops stacktrace and evaluation trace hints", () => { + const notes = responseNotes(createLogsMonitorEvidence()); + + expect(notes).not.toContain("stacktrace"); + expect(notes).not.toContain("trace:bfa06a9c589f4d4f89236378d2194adc"); + }); +}); diff --git a/packages/mcp-core/src/internal/formatting.ts b/packages/mcp-core/src/internal/formatting.ts index 6ce811051..36bacba56 100644 --- a/packages/mcp-core/src/internal/formatting.ts +++ b/packages/mcp-core/src/internal/formatting.ts @@ -1691,34 +1691,143 @@ function formatPrimitive(value: unknown): string | undefined { return isPrimitive(value) ? String(value) : undefined; } +const METRIC_ISSUE_OCCURRENCE_TYPE = 8001; + +type MetricAlertQuery = { + dataset?: string; + aggregate?: string; + query?: string; + timeWindowSeconds?: number; + environment?: string; + eventTypes: string[]; +}; + +type MetricAlertContext = { + evidenceData: Record; + query?: MetricAlertQuery; + monitorId?: string; + alertRuleId?: string; +}; + /** - * Extracts the Snuba query from metric alert evidence data. + * Reads metric alert evidence from a metric issue event. The event API + * camelCases occurrence payloads recursively, so evidence keys arrive as + * `dataSources[].queryObj.snubaQuery`, `detectorId`, and `alertId`. */ -function getMetricAlertSnubaQuery( +function getMetricAlertContext(event: Event): MetricAlertContext | undefined { + if (event.type !== "generic") { + return undefined; + } + const occurrence = (event as GenericEvent).occurrence; + const evidenceData = occurrence?.evidenceData; + if (occurrence?.type !== METRIC_ISSUE_OCCURRENCE_TYPE || !evidenceData) { + return undefined; + } + + return { + evidenceData, + query: getMetricAlertQuery(evidenceData), + monitorId: formatPrimitive(evidenceData.detectorId), + alertRuleId: formatPrimitive(evidenceData.alertId), + }; +} + +function getMetricAlertQuery( evidenceData: Record, -): Record | undefined { - const dataSources = evidenceData.data_sources; +): MetricAlertQuery | undefined { + const dataSources = evidenceData.dataSources; if (!Array.isArray(dataSources)) { return undefined; } for (const dataSource of dataSources) { - if (!isPlainObject(dataSource)) { + if (!isPlainObject(dataSource) || !isPlainObject(dataSource.queryObj)) { continue; } - const queryObj = dataSource.query_obj; - if (!isPlainObject(queryObj)) { + const snubaQuery = dataSource.queryObj.snubaQuery; + if (!isPlainObject(snubaQuery)) { continue; } - const snubaQuery = queryObj.snuba_query; - if (isPlainObject(snubaQuery)) { - return snubaQuery; - } + const timeWindow = snubaQuery.timeWindow; + return { + dataset: formatPrimitive(snubaQuery.dataset), + aggregate: formatPrimitive(snubaQuery.aggregate), + query: formatPrimitive(snubaQuery.query)?.trim() || undefined, + timeWindowSeconds: + typeof timeWindow === "number" ? timeWindow : undefined, + environment: formatPrimitive(snubaQuery.environment), + eventTypes: Array.isArray(snubaQuery.eventTypes) + ? snubaQuery.eventTypes.filter( + (eventType): eventType is string => typeof eventType === "string", + ) + : [], + }; } return undefined; } +type MetricAlertDatasetKind = "errors" | "logs" | "spans" | "metrics"; + +/** Mirrors the Sentry UI's backend dataset -> detector dataset mapping. */ +function getMetricAlertDatasetKind( + query: MetricAlertQuery, +): MetricAlertDatasetKind | undefined { + if (query.dataset === "events") { + return "errors"; + } + if (query.dataset === "events_analytics_platform") { + if (query.eventTypes.includes("trace_item_log")) { + return "logs"; + } + if (query.eventTypes.includes("trace_item_metric")) { + return "metrics"; + } + return "spans"; + } + return undefined; +} + +const METRIC_ALERT_SEARCH_TOOLS: Record< + MetricAlertDatasetKind, + { toolName: string; label: string } +> = { + errors: { toolName: "search_errors", label: "Errors" }, + logs: { toolName: "search_logs", label: "Logs" }, + spans: { toolName: "search_traces", label: "Spans" }, + metrics: { toolName: "search_metrics", label: "Application metrics" }, +}; + +/** Returns the window the monitor aggregated over for this occurrence. */ +function getMetricAlertEvaluatedWindow( + event: Event, + query: MetricAlertQuery | undefined, +): { start: Date; end: Date } | undefined { + const dateCreated = (event as GenericEvent).dateCreated; + if (!dateCreated || query?.timeWindowSeconds == null) { + return undefined; + } + const end = new Date(dateCreated); + if (Number.isNaN(end.getTime())) { + return undefined; + } + return { + start: new Date(end.getTime() - query.timeWindowSeconds * 1000), + end, + }; +} + +function formatSearchFilterValue(value: string): string { + return /[\s"(),:]/.test(value) ? JSON.stringify(value) : value; +} + +/** Smallest relative search period that reaches back to `start`. */ +function formatPeriodSince(start: Date, now: number): string { + const elapsedMs = Math.max(now - start.getTime(), 0); + const hours = Math.max(Math.ceil(elapsedMs / 3_600_000), 1); + return hours <= 48 ? `${hours}h` : `${Math.ceil(hours / 24)}d`; +} + function formatMetricAlertCondition( condition: Record, ): string | undefined { @@ -1763,32 +1872,46 @@ function formatMetricAlertValue(value: unknown): string | undefined { } function formatMetricAlertDetails( - evidenceData: Record, + event: Event, + context: MetricAlertContext, ): string { + const { evidenceData, query } = context; const parts: string[] = ["### Metric Alert Details", ""]; - const snubaQuery = getMetricAlertSnubaQuery(evidenceData); - if (snubaQuery) { - if (snubaQuery.dataset != null) { - parts.push(`**Dataset**: ${formatUnknown(snubaQuery.dataset)}`); + if (context.monitorId) { + parts.push(`**Monitor ID**: ${context.monitorId}`); + } + if (context.alertRuleId) { + parts.push(`**Alert Rule ID**: ${context.alertRuleId}`); + } + + if (query) { + const datasetKind = getMetricAlertDatasetKind(query); + const dataset = datasetKind + ? METRIC_ALERT_SEARCH_TOOLS[datasetKind].label + : query.dataset; + if (dataset) { + parts.push(`**Dataset**: ${dataset}`); } - if (snubaQuery.aggregate != null) { - parts.push(`**Aggregate**: ${formatUnknown(snubaQuery.aggregate)}`); + if (query.aggregate) { + parts.push(`**Aggregate**: ${query.aggregate}`); } - const query = - snubaQuery.query != null ? formatUnknown(snubaQuery.query) : ""; - if (query.length > 0) { - parts.push(`**Query**: \`${query}\``); + if (query.query) { + parts.push(`**Query**: \`${query.query}\``); } - if (snubaQuery.time_window != null) { - parts.push( - `**Interval**: ${formatUnknown(snubaQuery.time_window)} second(s)`, - ); + if (query.timeWindowSeconds != null) { + parts.push(`**Interval**: ${query.timeWindowSeconds} second(s)`); } - if (snubaQuery.environment != null) { - parts.push(`**Environment**: ${formatUnknown(snubaQuery.environment)}`); + if (query.environment) { + parts.push(`**Environment**: ${query.environment}`); } - parts.push(""); + } + + const evaluatedWindow = getMetricAlertEvaluatedWindow(event, query); + if (evaluatedWindow) { + parts.push( + `**Evaluated Window**: ${evaluatedWindow.start.toISOString()} to ${evaluatedWindow.end.toISOString()}`, + ); } const formattedValue = formatMetricAlertValue(evidenceData.value); @@ -1807,14 +1930,95 @@ function formatMetricAlertDetails( } } - if (evidenceData.alert_id != null) { - parts.push(`**Alert Rule ID**: ${formatUnknown(evidenceData.alert_id)}`); - } - parts.push(""); return `${parts.join("\n")}\n`; } +/** + * Follow-up notes for metric issues: the monitor configuration and a search + * call that reproduces the monitor query over the evaluated window. + */ +function buildMetricAlertResponseNotes({ + organizationSlug, + issue, + event, + context, + experimentalMode, + availableToolNames, + directToolNames, +}: { + organizationSlug: string; + issue: Issue; + event: Event; + context: MetricAlertContext; + experimentalMode: boolean; + availableToolNames?: ReadonlySet; + directToolNames?: ReadonlySet; +}): string[] { + const notes: string[] = []; + + if (context.monitorId) { + const monitorInstruction = formatToolCallInstruction({ + toolName: "get_metric_monitor_details", + arguments: { organizationSlug, monitorId: context.monitorId }, + showArguments: true, + experimentalMode, + availableToolNames, + directToolNames, + fallbackInstruction: "", + }); + if (monitorInstruction) { + notes.push( + `Metric Monitor configuration and connected Alerts: ${monitorInstruction}`, + ); + } + } + + const query = context.query; + const datasetKind = query ? getMetricAlertDatasetKind(query) : undefined; + const evaluatedWindow = getMetricAlertEvaluatedWindow(event, query); + if (!query || !datasetKind || !evaluatedWindow) { + return notes; + } + + const { toolName, label } = METRIC_ALERT_SEARCH_TOOLS[datasetKind]; + // Sentry search binds implicit AND tighter than OR, so group an OR query + // before appending the environment filter. + const monitorQuery = + query.query && query.environment && /\bOR\b/.test(query.query) + ? `(${query.query})` + : query.query; + const searchQuery = [ + monitorQuery, + query.environment + ? `environment:${formatSearchFilterValue(query.environment)}` + : undefined, + ] + .filter(Boolean) + .join(" "); + const searchInstruction = formatToolCallInstruction({ + toolName, + arguments: { + organizationSlug, + projectSlug: issue.project.slug, + ...(searchQuery ? { query: searchQuery } : {}), + period: formatPeriodSince(evaluatedWindow.start, Date.now()), + }, + showArguments: true, + experimentalMode, + availableToolNames, + directToolNames, + fallbackInstruction: "", + }); + if (searchInstruction) { + notes.push( + `${label} matched by the monitor query: ${searchInstruction}, then focus on ${evaluatedWindow.start.toISOString()} to ${evaluatedWindow.end.toISOString()}`, + ); + } + + return notes; +} + /** * Formats generic event output (performance regressions, metric-based issues). * Generic events don't have traditional error entries, but have occurrence data @@ -1835,11 +2039,13 @@ function formatGenericEventOutput(event: Event): string { return ""; } - const evidenceData = occurrence.evidenceData; - if (occurrence.type === 8001 && evidenceData) { - return formatMetricAlertDetails(evidenceData); + const metricAlertContext = getMetricAlertContext(event); + if (metricAlertContext) { + return formatMetricAlertDetails(event, metricAlertContext); } + const evidenceData = occurrence.evidenceData; + // Add a section header for performance regression details if (evidenceData) { parts.push("### Performance Regression Details"); @@ -2274,9 +2480,26 @@ export function buildIssueResponseNotes({ notes.push( `Commit message issue reference: \`Fixes ${commitIssueReference}\` automatically closes the issue when the commit is merged.`, ); - notes.push( - "The stacktrace includes first-party application code and third-party code. First-party frames are usually the best starting point for triage.", - ); + // Metric issues have no stacktrace, and their trace context belongs to the + // monitor evaluation rather than the data that breached the threshold. + const metricAlertContext = getMetricAlertContext(event); + if (metricAlertContext) { + notes.push( + ...buildMetricAlertResponseNotes({ + organizationSlug, + issue, + event, + context: metricAlertContext, + experimentalMode: experimentalMode ?? false, + availableToolNames, + directToolNames, + }), + ); + } else { + notes.push( + "The stacktrace includes first-party application code and third-party code. First-party frames are usually the best starting point for triage.", + ); + } if (aiConversations && aiConversations.length > 0) { notes.push( ...buildAIConversationResponseNotes({ @@ -2328,7 +2551,7 @@ export function buildIssueResponseNotes({ notes.push(`Thread stacktrace lookup: ${stacktraceInstruction}`); } } - if (traceId) { + if (traceId && !metricAlertContext) { const traceDetailsInstruction = formatToolCallInstruction({ toolName: "get_sentry_resource", arguments: { diff --git a/packages/mcp-core/src/internal/tool-helpers/tool-call-formatting.ts b/packages/mcp-core/src/internal/tool-helpers/tool-call-formatting.ts index 883f1ffbb..553ca8eeb 100644 --- a/packages/mcp-core/src/internal/tool-helpers/tool-call-formatting.ts +++ b/packages/mcp-core/src/internal/tool-helpers/tool-call-formatting.ts @@ -65,10 +65,13 @@ export function formatToolCall({ /** * Formats user-facing tool guidance as a direct call, catalog gateway call, or * fallback message based on the current session's available/direct tools. + * `showArguments` renders the full call when the arguments are concrete values + * the agent can run as-is rather than placeholders. */ export function formatToolCallInstruction({ toolName, - arguments: _args = {}, + arguments: args = {}, + showArguments = false, experimentalMode, purpose, availableToolNames, @@ -77,6 +80,7 @@ export function formatToolCallInstruction({ }: { toolName: string; arguments?: Record; + showArguments?: boolean; experimentalMode: boolean; purpose?: string; availableToolNames?: ReadonlySet; @@ -88,11 +92,15 @@ export function formatToolCallInstruction({ availableToolNames, ); + const toolReference = showArguments + ? formatToolCall({ toolName, arguments: args }) + : toolName; + if ( targetAvailable && isDirectTool(toolName, experimentalMode, directToolNames) ) { - return `Use the Sentry tool \`${toolName}\`${formatPurpose(purpose)}`; + return `Use the Sentry tool \`${toolReference}\`${formatPurpose(purpose)}`; } const catalogGatewayAvailable = @@ -102,7 +110,7 @@ export function formatToolCallInstruction({ isDirectTool("execute_sentry_tool", experimentalMode, directToolNames); if (targetAvailable && catalogGatewayAvailable) { - return `Use the Sentry tool \`${toolName}\`${formatPurpose(purpose)}`; + return `Use the Sentry tool \`${toolReference}\`${formatPurpose(purpose)}`; } return (