Skip to content
Open
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
20 changes: 12 additions & 8 deletions src/tools/tfa-rca-utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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):
* `<UI_BASE>/builds/<buildUuid>?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() +
Expand All @@ -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. */
Expand Down
30 changes: 29 additions & 1 deletion src/tools/tfa-rca-utils/trigger-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string, string>,
): Promise<string> {
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,
Expand Down Expand Up @@ -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),
};
}
15 changes: 13 additions & 2 deletions tests/tools/triggerRcaReport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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" },
Expand All @@ -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();
Expand Down Expand Up @@ -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" },
Expand All @@ -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 () => {
Expand Down