Skip to content

Commit 058ad08

Browse files
Merge pull request #136 from tech-sushant/rca-agent
feat: Add RCA tools for fetching and formatting Root Cause Analysis data
2 parents fdf7614 + 82cc762 commit 058ad08

File tree

8 files changed

+732
-0
lines changed

8 files changed

+732
-0
lines changed

src/server-factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import addSelfHealTools from "./tools/selfheal.js";
1818
import addAppLiveTools from "./tools/applive.js";
1919
import { setupOnInitialized } from "./oninitialized.js";
2020
import { BrowserStackConfig } from "./lib/types.js";
21+
import addRCATools from "./tools/rca-agent.js";
2122

2223
/**
2324
* Wrapper class for BrowserStack MCP Server
@@ -57,6 +58,7 @@ export class BrowserStackMcpServer {
5758
addFailureLogsTools,
5859
addAutomateTools,
5960
addSelfHealTools,
61+
addRCATools,
6062
];
6163

6264
toolAdders.forEach((adder) => {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { z } from "zod";
2+
import { TestStatus } from "./types.js";
3+
4+
export const FETCH_RCA_PARAMS = {
5+
testId: z
6+
.array(z.string())
7+
.max(3)
8+
.describe(
9+
"Array of test IDs to fetch RCA data for (maximum 3 IDs). If not provided, use the listTestIds tool get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed."
10+
),
11+
};
12+
13+
export const GET_BUILD_ID_PARAMS = {
14+
projectName: z
15+
.string()
16+
.describe(
17+
"The Browserstack project name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions"
18+
),
19+
buildName: z
20+
.string()
21+
.describe(
22+
"The Browserstack build name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions"
23+
),
24+
};
25+
26+
export const LIST_TEST_IDS_PARAMS = {
27+
buildId: z
28+
.string()
29+
.describe(
30+
"The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name"
31+
),
32+
status: z
33+
.nativeEnum(TestStatus)
34+
.describe(
35+
"Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status"
36+
),
37+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Utility function to format RCA data for better readability
2+
export function formatRCAData(rcaData: any): string {
3+
if (!rcaData || !rcaData.testCases || rcaData.testCases.length === 0) {
4+
return "No RCA data available.";
5+
}
6+
7+
let output = "## Root Cause Analysis Report\n\n";
8+
9+
rcaData.testCases.forEach((testCase: any, index: number) => {
10+
// Show test case ID with smaller heading
11+
output += `### Test Case ${index + 1}\n`;
12+
output += `**Test ID:** ${testCase.id}\n`;
13+
output += `**Status:** ${testCase.state}\n\n`;
14+
15+
// Access RCA data from the correct path
16+
const rca = testCase.rcaData?.rcaData;
17+
18+
if (rca) {
19+
if (rca.root_cause) {
20+
output += `**Root Cause:** ${rca.root_cause}\n\n`;
21+
}
22+
23+
if (rca.failure_type) {
24+
output += `**Failure Type:** ${rca.failure_type}\n\n`;
25+
}
26+
27+
if (rca.description) {
28+
output += `**Detailed Analysis:**\n${rca.description}\n\n`;
29+
}
30+
31+
if (rca.possible_fix) {
32+
output += `**Recommended Fix:**\n${rca.possible_fix}\n\n`;
33+
}
34+
} else if (testCase.rcaData?.error) {
35+
output += `**Error:** ${testCase.rcaData.error}\n\n`;
36+
} else if (testCase.state === "failed") {
37+
output += `**Note:** RCA analysis failed or is not available for this test case.\n\n`;
38+
}
39+
40+
output += "---\n\n";
41+
});
42+
43+
return output;
44+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export async function getBuildId(
2+
projectName: string,
3+
buildName: string,
4+
username: string,
5+
accessKey: string,
6+
): Promise<string> {
7+
const url = new URL(
8+
"https://api-automation.browserstack.com/ext/v1/builds/latest",
9+
);
10+
url.searchParams.append("project_name", projectName);
11+
url.searchParams.append("build_name", buildName);
12+
url.searchParams.append("user_name", username);
13+
14+
const authHeader =
15+
"Basic " + Buffer.from(`${username}:${accessKey}`).toString("base64");
16+
17+
const response = await fetch(url.toString(), {
18+
headers: {
19+
Authorization: authHeader,
20+
"Content-Type": "application/json",
21+
},
22+
});
23+
24+
if (!response.ok) {
25+
throw new Error(
26+
`Failed to fetch build ID: ${response.status} ${response.statusText}`,
27+
);
28+
}
29+
30+
const data = await response.json();
31+
return data.build_id;
32+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import logger from "../../logger.js";
2+
import { TestStatus, FailedTestInfo, TestRun, TestDetails } from "./types.js";
3+
4+
export async function getTestIds(
5+
buildId: string,
6+
authString: string,
7+
status?: TestStatus,
8+
): Promise<FailedTestInfo[]> {
9+
const baseUrl = `https://api-automation.browserstack.com/ext/v1/builds/${buildId}/testRuns`;
10+
let url = status ? `${baseUrl}?test_statuses=${status}` : baseUrl;
11+
let allFailedTests: FailedTestInfo[] = [];
12+
let requestNumber = 0;
13+
14+
// Construct Basic auth header
15+
const encodedCredentials = Buffer.from(authString).toString("base64");
16+
const authHeader = `Basic ${encodedCredentials}`;
17+
18+
try {
19+
while (true) {
20+
requestNumber++;
21+
22+
const response = await fetch(url, {
23+
headers: {
24+
Authorization: authHeader,
25+
"Content-Type": "application/json",
26+
},
27+
});
28+
29+
if (!response.ok) {
30+
throw new Error(
31+
`Failed to fetch test runs: ${response.status} ${response.statusText}`,
32+
);
33+
}
34+
35+
const data = (await response.json()) as TestRun;
36+
37+
// Extract failed IDs from current page
38+
if (data.hierarchy && data.hierarchy.length > 0) {
39+
const currentFailedTests = extractFailedTestIds(data.hierarchy);
40+
allFailedTests = allFailedTests.concat(currentFailedTests);
41+
}
42+
43+
// Check for pagination termination conditions
44+
if (!data.pagination?.has_next || !data.pagination.next_page) {
45+
break;
46+
}
47+
48+
// Safety limit to prevent runaway requests
49+
if (requestNumber >= 5) {
50+
break;
51+
}
52+
53+
// Prepare next request
54+
url = `${baseUrl}?next_page=${encodeURIComponent(data.pagination.next_page)}`;
55+
}
56+
57+
// Return unique failed test IDs
58+
return allFailedTests;
59+
} catch (error) {
60+
logger.error("Error fetching failed tests:", error);
61+
throw error;
62+
}
63+
}
64+
65+
// Recursive function to extract failed test IDs from hierarchy
66+
function extractFailedTestIds(hierarchy: TestDetails[]): FailedTestInfo[] {
67+
let failedTests: FailedTestInfo[] = [];
68+
69+
for (const node of hierarchy) {
70+
if (node.details?.status === "failed" && node.details?.run_count) {
71+
if (node.details?.observability_url) {
72+
const idMatch = node.details.observability_url.match(/details=(\d+)/);
73+
if (idMatch) {
74+
failedTests.push({
75+
test_id: idMatch[1],
76+
test_name: node.display_name || `Test ${idMatch[1]}`,
77+
});
78+
}
79+
}
80+
}
81+
82+
if (node.children && node.children.length > 0) {
83+
failedTests = failedTests.concat(extractFailedTestIds(node.children));
84+
}
85+
}
86+
87+
return failedTests;
88+
}

0 commit comments

Comments
 (0)