Skip to content
Draft
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
9 changes: 8 additions & 1 deletion packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,12 +671,19 @@ export class SentryApiService {
return getTraceUrlUtil(this.host, organizationSlug, traceId, this.protocol);
}

getReplayUrl(organizationSlug: string, replayId: string): string {
getReplayUrl(
organizationSlug: string,
replayId: string,
options?: {
eventTimestamp?: string;
},
): string {
return getReplayUrlUtil(
this.host,
organizationSlug,
replayId,
this.protocol,
options,
);
}

Expand Down
10 changes: 9 additions & 1 deletion packages/mcp-core/src/internal/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1976,8 +1976,16 @@ function formatIssueReplayOutput({
const lines: string[] = ["## Session Replay", ""];

if (attachedReplayId) {
const eventDateCreated =
"dateCreated" in event && typeof event.dateCreated === "string"
? event.dateCreated
: undefined;
lines.push(
`**Attached Replay**: ${apiService.getReplayUrl(organizationSlug, attachedReplayId)}`,
`**Attached Replay**: ${apiService.getReplayUrl(
organizationSlug,
attachedReplayId,
eventDateCreated ? { eventTimestamp: eventDateCreated } : undefined,
)}`,
);
}

Expand Down
54 changes: 54 additions & 0 deletions packages/mcp-core/src/internal/url-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,60 @@ describe("parseSentryUrl", () => {
});
});

describe("feedback URLs", () => {
it("parses feedback URL with feedbackSlug as issue", () => {
expect(
parseSentryUrl(
"https://sentry.sentry.io/feedback/?feedbackSlug=javascript%3A7513887620",
),
).toMatchInlineSnapshot(`
{
"issueId": "7513887620",
"organizationSlug": "sentry",
"type": "issue",
}
`);
});

it("parses feedback URL with organizations path", () => {
expect(
parseSentryUrl(
"https://us.sentry.io/organizations/my-org/feedback/?feedbackSlug=my-project%3A9876543210",
),
).toMatchInlineSnapshot(`
{
"issueId": "9876543210",
"organizationSlug": "my-org",
"type": "issue",
}
`);
});

it("returns unknown for feedback URL without feedbackSlug", () => {
expect(
parseSentryUrl("https://my-org.sentry.io/feedback/"),
).toMatchInlineSnapshot(`
{
"organizationSlug": "my-org",
"type": "unknown",
}
`);
});

it("returns unknown for feedback URL with non-numeric groupId", () => {
expect(
parseSentryUrl(
"https://my-org.sentry.io/feedback/?feedbackSlug=project%3Anot-a-number",
),
).toMatchInlineSnapshot(`
{
"organizationSlug": "my-org",
"type": "unknown",
}
`);
});
});

describe("unknown URLs", () => {
it("returns unknown for unrecognized path", () => {
expect(
Expand Down
20 changes: 20 additions & 0 deletions packages/mcp-core/src/internal/url-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,26 @@ function identifyResource(
}
}

// Feedback URL: /feedback/?feedbackSlug={projectSlug}:{groupId}
const feedbackIndex = pathParts.indexOf("feedback");
if (feedbackIndex !== -1) {
const feedbackSlug = parsedUrl.searchParams.get("feedbackSlug");
if (feedbackSlug) {
// feedbackSlug format is "{projectSlug}:{groupId}" where groupId is numeric
const colonIndex = feedbackSlug.lastIndexOf(":");
if (colonIndex !== -1) {
const groupId = feedbackSlug.slice(colonIndex + 1);
if (groupId && /^\d+$/.test(groupId)) {
return {
type: "issue",
organizationSlug,
issueId: groupId,
};
}
}
}
}

// Could not identify resource type
return {
type: "unknown",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/tools/get-issue-details.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ describe("get_issue_details", () => {
expect(replaySection).toMatchInlineSnapshot(`
"## Session Replay

**Attached Replay**: https://sentry-mcp-evals.sentry.io/explore/replays/7e07485f12f9416b8b1426260799b51f/
**Attached Replay**: https://sentry-mcp-evals.sentry.io/explore/replays/7e07485f12f9416b8b1426260799b51f/?event_t=2025-10-02T12%3A00%3A00.000Z
**Related Replay Count**: 2

### Other Related Replays
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/tools/get-sentry-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ function resolveFromParsedUrl(
}
throw new UserInputError(
"Could not determine resource type from URL. " +
"Supported URL patterns: issues, events, traces, AI conversations, profiles, replays, monitors, and releases.",
"Supported URL patterns: issues, events, traces, AI conversations, profiles, replays, monitors, releases, and feedback.",
);
}

Expand Down
21 changes: 21 additions & 0 deletions packages/mcp-core/src/utils/url-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,27 @@ describe("url-utils", () => {
"https://sentry.example.com/organizations/myorg/explore/replays/abc123/",
);
});

it("should append event_t query param when eventTimestamp is provided", () => {
const result = getReplayUrl("us.sentry.io", "myorg", "abc123", "https", {
eventTimestamp: "2024-01-15T10:23:45.123456Z",
});
expect(result).toBe(
"https://myorg.sentry.io/explore/replays/abc123/?event_t=2024-01-15T10%3A23%3A45.123456Z",
);
});

it("should not append event_t when eventTimestamp is undefined", () => {
const result = getReplayUrl("us.sentry.io", "myorg", "abc123", "https", {
eventTimestamp: undefined,
});
expect(result).toBe("https://myorg.sentry.io/explore/replays/abc123/");
});

it("should not append event_t when options is omitted", () => {
const result = getReplayUrl("us.sentry.io", "myorg", "abc123");
expect(result).toBe("https://myorg.sentry.io/explore/replays/abc123/");
});
});

describe("getEventsExplorerUrl", () => {
Expand Down
25 changes: 19 additions & 6 deletions packages/mcp-core/src/utils/url-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,20 +529,33 @@ export function getTraceUrl(
* @param host The Sentry host (may include regional subdomain for API access)
* @param organizationSlug Organization identifier
* @param replayId Replay identifier
* @param protocol Protocol (https or http)
* @param options Optional parameters
* @param options.eventTimestamp ISO timestamp of a related event; when provided the replay player jumps to that moment using `?event_t=`
* @returns The complete replay URL
*/
export function getReplayUrl(
host: string,
organizationSlug: string,
replayId: string,
protocol: SentryProtocol = "https",
options?: {
eventTimestamp?: string;
},
): string {
return getSentryWebBaseUrl(
host,
organizationSlug,
`/explore/replays/${replayId}/`,
protocol,
);
const path = `/explore/replays/${replayId}/`;

if (options?.eventTimestamp) {
const params = new URLSearchParams({ event_t: options.eventTimestamp });
return getSentryWebBaseUrl(
host,
organizationSlug,
`${path}?${params.toString()}`,
protocol,
);
}

return getSentryWebBaseUrl(host, organizationSlug, path, protocol);
}

/**
Expand Down
Loading