diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 8a7ae867..65feb298 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -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, ); } diff --git a/packages/mcp-core/src/internal/formatting.ts b/packages/mcp-core/src/internal/formatting.ts index 060e1dc1..391ac180 100644 --- a/packages/mcp-core/src/internal/formatting.ts +++ b/packages/mcp-core/src/internal/formatting.ts @@ -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, + )}`, ); } diff --git a/packages/mcp-core/src/internal/url-helpers.test.ts b/packages/mcp-core/src/internal/url-helpers.test.ts index 5d2ac305..2138d75c 100644 --- a/packages/mcp-core/src/internal/url-helpers.test.ts +++ b/packages/mcp-core/src/internal/url-helpers.test.ts @@ -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( diff --git a/packages/mcp-core/src/internal/url-helpers.ts b/packages/mcp-core/src/internal/url-helpers.ts index 1546b3d5..01854e78 100644 --- a/packages/mcp-core/src/internal/url-helpers.ts +++ b/packages/mcp-core/src/internal/url-helpers.ts @@ -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", diff --git a/packages/mcp-core/src/tools/get-issue-details.test.ts b/packages/mcp-core/src/tools/get-issue-details.test.ts index 6c627c20..6986c98f 100644 --- a/packages/mcp-core/src/tools/get-issue-details.test.ts +++ b/packages/mcp-core/src/tools/get-issue-details.test.ts @@ -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 diff --git a/packages/mcp-core/src/tools/get-sentry-resource.ts b/packages/mcp-core/src/tools/get-sentry-resource.ts index 8db0715c..f162b8a5 100644 --- a/packages/mcp-core/src/tools/get-sentry-resource.ts +++ b/packages/mcp-core/src/tools/get-sentry-resource.ts @@ -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.", ); } diff --git a/packages/mcp-core/src/utils/url-utils.test.ts b/packages/mcp-core/src/utils/url-utils.test.ts index c654df5c..2dfe19ab 100644 --- a/packages/mcp-core/src/utils/url-utils.test.ts +++ b/packages/mcp-core/src/utils/url-utils.test.ts @@ -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", () => { diff --git a/packages/mcp-core/src/utils/url-utils.ts b/packages/mcp-core/src/utils/url-utils.ts index 0785b0be..50605853 100644 --- a/packages/mcp-core/src/utils/url-utils.ts +++ b/packages/mcp-core/src/utils/url-utils.ts @@ -529,6 +529,9 @@ 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( @@ -536,13 +539,23 @@ export function getReplayUrl( 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); } /**