Skip to content
Open
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
5 changes: 5 additions & 0 deletions packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
getContinuousProfileUrl as getContinuousProfileUrlUtil,
getDashboardUrl as getDashboardUrlUtil,
getIssueUrl as getIssueUrlUtil,
getLogUrl as getLogUrlUtil,
getMonitorUrl as getMonitorUrlUtil,
getPreprodSnapshotUrl as getPreprodSnapshotUrlUtil,
getProfileUrl as getProfileUrlUtil,
Expand Down Expand Up @@ -1204,6 +1205,10 @@ export class SentryApiService {
return getTraceUrlUtil(this.host, organizationSlug, traceId, this.protocol);
}

getLogUrl(organizationSlug: string, logId: string): string {
return getLogUrlUtil(this.host, organizationSlug, logId, this.protocol);
}

getReplayUrl(organizationSlug: string, replayId: string): string {
return getReplayUrlUtil(
this.host,
Expand Down
60 changes: 60 additions & 0 deletions packages/mcp-core/src/tools/catalog/search-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2657,6 +2657,65 @@ describe("search_events", () => {
expect(mockGenerateText).toHaveBeenCalled();
expect(result).toContain("Connection failed to database");
expect(result).toContain("🔴 [ERROR]");
expect(result).toContain(
"- **Log URL**: https://test-org.sentry.io/explore/logs/?project=-1&logsPinned=log1",
);
});

it("does not request log IDs for aggregate logs queries", async () => {
let eventsRequestUrl: URL | undefined;

mockGenerateText.mockResolvedValueOnce(
mockAIResponse(
"logs",
"severity:error",
["severity", "count()"],
undefined,
"-count()",
),
);

mswServer.use(
http.get(
"https://sentry.io/api/0/organizations/test-org/events/",
({ request }) => {
eventsRequestUrl = new URL(request.url);
return HttpResponse.json({
data: [{ severity: "error", "count()": 3 }],
});
},
),
);

const result = await searchEvents.handler(
{
organizationSlug: "test-org",
regionUrl: null,
projectSlug: null,
query: "count error logs by severity",
dataset: "errors",
fields: null,
sort: "-count()",
period: "14d",
limit: 10,
includeExplanation: false,
},
{
constraints: {
organizationSlug: null,
regionUrl: null,
projectSlug: null,
},
accessToken: "test-token",
userId: "1",
},
);

expect(eventsRequestUrl!.searchParams.getAll("field")).toEqual([
"severity",
"count()",
]);
expect(result).not.toContain("Log URL");
});

it("should repair direct search params with the agent when available", async () => {
Expand Down Expand Up @@ -2722,6 +2781,7 @@ describe("search_events", () => {
"timestamp",
"message",
"severity",
"id",
]);
expect(result).toContain("Connection failed to database");
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,10 @@ export function formatLogResults(params: FormatEventResultsParams): string {

output += `### Log ${i + 1}\n`;

if (typeof event.id === "string" && event.id) {
output += `- **Log URL**: ${apiService.getLogUrl(organizationSlug, event.id)}\n`;
}

// Display priority fields first
for (const field of priorityFields) {
if (
Expand Down
10 changes: 7 additions & 3 deletions packages/mcp-core/src/tools/support/search-events/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,13 @@ function buildRequestFields(
dataset: PublicEventsDataset | "replays",
fields: string[],
): string[] {
return dataset !== "replays" &&
isMetricsDataset(dataset) &&
!isAggregateQuery(fields)
if (dataset === "replays" || isAggregateQuery(fields)) {
return fields;
}
if (dataset === "logs") {
return Array.from(new Set([...fields, "id"]));
}
return isMetricsDataset(dataset)
? Array.from(new Set([...fields, ...TRACE_METRICS_SAMPLE_IDENTITY_FIELDS]))
: fields;
}
Expand Down
17 changes: 17 additions & 0 deletions packages/mcp-core/src/utils/url-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getEventsExplorerUrl,
getIssuesSearchUrl,
getIssueUrl,
getLogUrl,
getMonitorUrl,
getPreprodSnapshotUrl,
getReleaseUrl,
Expand Down Expand Up @@ -309,6 +310,22 @@ describe("url-utils", () => {
});
});

describe("getLogUrl", () => {
it("pins the log in the logs explorer for SaaS", () => {
const result = getLogUrl("us.sentry.io", "myorg", "abc123");
expect(result).toBe(
"https://myorg.sentry.io/explore/logs/?project=-1&logsPinned=abc123",
);
});

it("pins the log in the logs explorer for self-hosted", () => {
const result = getLogUrl("sentry.example.com", "myorg", "abc123");
expect(result).toBe(
"https://sentry.example.com/organizations/myorg/explore/logs/?project=-1&logsPinned=abc123",
);
});
});

describe("getReplaysSearchUrl", () => {
it("should handle regional URLs correctly for SaaS", () => {
const result = getReplaysSearchUrl("us.sentry.io", "myorg", {
Expand Down
15 changes: 15 additions & 0 deletions packages/mcp-core/src/utils/url-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,21 @@ export function getTraceUrl(
);
}

export function getLogUrl(
host: string,
organizationSlug: string,
logId: string,
protocol: SentryProtocol = "https",
): string {
const urlParams = new URLSearchParams({ project: "-1", logsPinned: logId });
return getSentryWebBaseUrl(
host,
organizationSlug,
`/explore/logs/?${urlParams.toString()}`,
protocol,
);
}

/**
* Generates a Sentry replay URL.
* @param host The Sentry host (may include regional subdomain for API access)
Expand Down
Loading