From fb707296d4d141e3ae392509f2002e38b07d9e03 Mon Sep 17 00:00:00 2001 From: Junior Date: Sun, 10 May 2026 01:42:28 +0000 Subject: [PATCH] fix: use SENTRY_ORG_SLUG for footer Sentry links Replace numeric org ID extraction (getDsnOrgId, client orgId) with SENTRY_ORG_SLUG env var. For SaaS DSNs, emit subdomain-routed URLs (https://{slug}.sentry.io/...) instead of the legacy /organizations/ path. Self-hosted retains /organizations/{slug}/ with the slug. Closes #320 Co-Authored-By: Claude (anthropic/claude-opus-4.6) --- packages/junior/src/chat/slack/footer.ts | 26 +++++++------- packages/junior/src/cli/init.ts | 1 + .../unit/slack/footer-sentry-link.test.ts | 34 ++++++++++++++++--- 3 files changed, 43 insertions(+), 18 deletions(-) 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", }, ],