diff --git a/packages/junior/src/chat/slack/footer.ts b/packages/junior/src/chat/slack/footer.ts index 78ba2a6d..a7ac08f7 100644 --- a/packages/junior/src/chat/slack/footer.ts +++ b/packages/junior/src/chat/slack/footer.ts @@ -3,7 +3,6 @@ import type { TurnThinkingSelection } from "@/chat/services/turn-thinking-level" import type { AgentTurnUsage } from "@/chat/usage"; const SENTRY_CONVERSATION_SEARCH_STATS_PERIOD = "14d"; -const ORG_ID_HOST_RE = /^o(\d+)\./; interface SlackMrkdwnTextObject { text: string; @@ -55,19 +54,13 @@ function escapeSlackLinkUrl(url: string): string { .replaceAll(">", "%3E"); } -function toOptionalString(value: unknown): string | undefined { - if (typeof value === "number" && Number.isFinite(value)) { - return String(value); - } - return typeof value === "string" && value.trim() ? value.trim() : undefined; -} - function quoteSentrySearchValue(value: string): string { return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; } -function getDsnOrgId(host: string | undefined): string | undefined { - return host?.match(ORG_ID_HOST_RE)?.[1]; +function getSentryOrgSlug(): string | undefined { + const slug = process.env.SENTRY_ORG_SLUG?.trim(); + return slug || undefined; } function isSentrySaasDsnHost(host: string): boolean { @@ -98,9 +91,8 @@ function getSentryConversationSearchUrl( return undefined; } - const orgId = - toOptionalString(client?.getOptions().orgId) ?? getDsnOrgId(dsn.host); - if (!orgId) { + const orgSlug = getSentryOrgSlug(); + if (!orgSlug) { return undefined; } @@ -112,7 +104,13 @@ function getSentryConversationSearchUrl( params.set("project", dsn.projectId); params.set("statsPeriod", SENTRY_CONVERSATION_SEARCH_STATS_PERIOD); - return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgId}/explore/traces/?${params.toString()}`; + const search = `explore/traces/?${params.toString()}`; + + if (isSentrySaasDsnHost(dsn.host)) { + return `https://${orgSlug}.sentry.io/${search}`; + } + + return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${search}`; } function formatSlackTokenCount(value: number): string { diff --git a/packages/junior/src/cli/init.ts b/packages/junior/src/cli/init.ts index 0f1c424b..6c9cf762 100644 --- a/packages/junior/src/cli/init.ts +++ b/packages/junior/src/cli/init.ts @@ -149,6 +149,7 @@ AI_VISION_MODEL= AI_WEB_SEARCH_MODEL= REDIS_URL= SENTRY_DSN= +SENTRY_ORG_SLUG= `, ); diff --git a/packages/junior/tests/unit/slack/footer-sentry-link.test.ts b/packages/junior/tests/unit/slack/footer-sentry-link.test.ts index 747d481a..4d8027d9 100644 --- a/packages/junior/tests/unit/slack/footer-sentry-link.test.ts +++ b/packages/junior/tests/unit/slack/footer-sentry-link.test.ts @@ -24,12 +24,14 @@ async function loadFooter() { } afterEach(() => { + delete process.env.SENTRY_ORG_SLUG; vi.doUnmock("@/chat/sentry"); vi.resetModules(); }); describe("Slack footer Sentry links", () => { - it("links the ID to an Explore traces search from the active SaaS DSN", async () => { + it("links the ID to an Explore traces search using org slug subdomain for SaaS", async () => { + process.env.SENTRY_ORG_SLUG = "my-org"; mockSentryClient({ dsn: { protocol: "https", @@ -53,14 +55,14 @@ describe("Slack footer Sentry links", () => { elements: [ { type: "mrkdwn", - text: "*ID:* ", + text: "*ID:* ", }, ], }, ]); }); - it("uses an explicit SDK orgId before the DSN host org ID", async () => { + it("leaves the ID plain when SENTRY_ORG_SLUG is not set even with numeric org data", async () => { mockSentryClient({ dsn: { protocol: "https", @@ -77,7 +79,31 @@ describe("Slack footer Sentry links", () => { items: [ { label: "ID", - url: "https://sentry.io/organizations/456/explore/traces/?query=gen_ai.conversation.id%3A%22conversation-1%22&project=4501&statsPeriod=14d", + value: "conversation-1", + }, + ], + }, + ); + }); + + it("uses /organizations/{slug}/ for self-hosted DSN", async () => { + process.env.SENTRY_ORG_SLUG = "my-org"; + mockSentryClient({ + dsn: { + protocol: "https", + host: "sentry.example.com", + projectId: "4501", + }, + }); + + const { buildSlackReplyFooter } = await loadFooter(); + + expect(buildSlackReplyFooter({ conversationId: "conversation-1" })).toEqual( + { + items: [ + { + label: "ID", + url: "https://sentry.example.com/organizations/my-org/explore/traces/?query=gen_ai.conversation.id%3A%22conversation-1%22&project=4501&statsPeriod=14d", value: "conversation-1", }, ],