diff --git a/src/tools/tfa-rca-utils/constants.ts b/src/tools/tfa-rca-utils/constants.ts index c913508..3ed100e 100644 --- a/src/tools/tfa-rca-utils/constants.ts +++ b/src/tools/tfa-rca-utils/constants.ts @@ -22,16 +22,20 @@ export function getO11yUiBaseUrl(): string { return appConfig.BROWSERSTACK_O11Y_UI_BASE_URL; } +/** Query that deep-links a dashboard build URL to the AI-report TFA sub-tab. */ +export const AI_REPORT_TFA_QUERY = "tab=ai_report&subTab=tfa"; + +/** Read a build's metadata — carries the canonical `observability_url`. */ +export const BUILD_DETAILS_PATH = "/ext/v1/builds/{buildUuid}"; + /** - * TRA UI deep-link for a build's AI report (confirmed shape, 2026-07-13): - * `/builds/?tab=ai_report&subTab=aitfa` — the AI-TFA - * sub-tab of the build's AI report. `{buildUuid}` is replaced with the - * caller-supplied build id. + * UUID-form deep-link — fallback when the canonical `observability_url` can't + * be read. The UUID URL 302-redirects and the redirect drops the query string, + * so prefer the canonical URL (see `trigger-report.ts`). */ -export const O11Y_UI_BUILD_PATH = - "/builds/{buildUuid}?tab=ai_report&subTab=aitfa"; +export const O11Y_UI_BUILD_PATH = `/builds/{buildUuid}?${AI_REPORT_TFA_QUERY}`; -/** Human-facing TRA UI link for one build's full report. */ +/** Human-facing TRA UI link for one build's full report (UUID fallback form). */ export function getO11yUiBuildUrl(buildUuid: string): string { return ( getO11yUiBaseUrl() + @@ -45,7 +49,7 @@ export function getO11yUiBuildUrl(buildUuid: string): string { * (build page → AI report → AI TFA sub-tab). */ export function getRcaViewGuidance(): string { - return `${getO11yUiBaseUrl()} — open the build's AI report (tab=ai_report, subTab=aitfa) to view the full RCA`; + return `${getO11yUiBaseUrl()} — open the build's AI report (tab=ai_report, subTab=tfa) to view the full RCA`; } /** Trigger (or read, when already complete) a build's Release Readiness report. */ diff --git a/src/tools/tfa-rca-utils/trigger-report.ts b/src/tools/tfa-rca-utils/trigger-report.ts index dd013c7..7f976c8 100644 --- a/src/tools/tfa-rca-utils/trigger-report.ts +++ b/src/tools/tfa-rca-utils/trigger-report.ts @@ -2,6 +2,8 @@ import { apiClient } from "../../lib/apiClient.js"; import { getBrowserStackAuth } from "../../lib/get-auth.js"; import { BrowserStackConfig } from "../../lib/types.js"; import { + AI_REPORT_TFA_QUERY, + BUILD_DETAILS_PATH, getO11yBaseUrl, getO11yUiBuildUrl, RELEASE_READINESS_TRIGGER_PATH, @@ -76,6 +78,32 @@ function mapTriggerError(status: number, data: unknown): TriggerRcaReportError { * a build via the o11y external API, returning a trimmed glimpse. Stateless: * nothing persists between calls. */ +/** + * "View report" link = the build's canonical `observability_url` + the TFA + * sub-tab. The UUID form 302-redirects and drops the query, loading the wrong + * sub-tab; the trigger response carries no URL, so read it from build metadata + * and fall back to the UUID deep-link only if that read fails. + */ +async function resolveViewReport( + buildUuid: string, + headers: Record, +): Promise { + try { + const url = + getO11yBaseUrl() + + BUILD_DETAILS_PATH.replace("{buildUuid}", encodeURIComponent(buildUuid)); + const resp = await apiClient.get({ url, headers, raise_error: false }); + const observabilityUrl = + resp.ok && typeof resp.data?.observability_url === "string" + ? resp.data.observability_url + : undefined; + if (observabilityUrl) return `${observabilityUrl}?${AI_REPORT_TFA_QUERY}`; + } catch { + // fall through to the UUID deep-link + } + return getO11yUiBuildUrl(buildUuid); +} + export async function triggerRcaReport( args: TriggerRcaReportArgs, config: BrowserStackConfig, @@ -119,6 +147,6 @@ export async function triggerRcaReport( totalPrs: summary.totalPrs, faultyPrNumbers: summary.faultyPrNumbers, failureReason: summary.failureReason, - viewReport: getO11yUiBuildUrl(args.buildUuid), + viewReport: await resolveViewReport(args.buildUuid, headers), }; } diff --git a/tests/tools/triggerRcaReport.test.ts b/tests/tools/triggerRcaReport.test.ts index 19201f2..d7d0b8e 100644 --- a/tests/tools/triggerRcaReport.test.ts +++ b/tests/tools/triggerRcaReport.test.ts @@ -36,6 +36,7 @@ const mockConfig = { }; const post = apiClient.post as Mock; +const get = apiClient.get as Mock; function ok(data: any, status = 200) { return { ok: true, status, data }; @@ -72,6 +73,10 @@ describe("triggerRcaReportTool", () => { it("success → trimmed glimpse with UI link; prs/workflows never echoed", async () => { post.mockResolvedValue(fullReport()); + // Canonical dashboard URL comes from the build's observability_url. + get.mockResolvedValue( + ok({ observability_url: `${UI_BASE}/projects/P+Name/builds/bname/1` }), + ); const result = await triggerRcaReportTool( { buildUuid: "b-1" }, @@ -91,7 +96,9 @@ describe("triggerRcaReportTool", () => { expect(payload.failureReason).toBe( "2 product regressions traced to PR #412", ); - expect(payload.viewReport).toBe(`${UI_BASE}/builds/b-1?tab=ai_report&subTab=aitfa`); + expect(payload.viewReport).toBe( + `${UI_BASE}/projects/P+Name/builds/bname/1?tab=ai_report&subTab=tfa`, + ); // Raw response is never echoed: no prs[]/workflows[] entries, no envelope. expect(payload.prs).toBeUndefined(); expect(payload.workflows).toBeUndefined(); @@ -132,6 +139,8 @@ describe("triggerRcaReportTool", () => { post.mockResolvedValue( ok({ state: "running", buildUuid: "b-2", triggeredAt: "now" }), ); + // Build metadata unavailable → fall back to the UUID deep-link (still subTab=tfa). + get.mockResolvedValue(nonOk(404)); const result = await triggerRcaReportTool( { buildUuid: "b-2" }, @@ -140,7 +149,9 @@ describe("triggerRcaReportTool", () => { const payload = JSON.parse(result.content[0].text as string); expect(payload.state).toBe("running"); expect(payload.verdict).toBeUndefined(); - expect(payload.viewReport).toBe(`${UI_BASE}/builds/b-2?tab=ai_report&subTab=aitfa`); + expect(payload.viewReport).toBe( + `${UI_BASE}/builds/b-2?tab=ai_report&subTab=tfa`, + ); }); it("403 plan/flag fence → clear domain error", async () => {