Skip to content

Commit 8c3a224

Browse files
committed
feat: Add RCA tools for fetching and formatting Root Cause Analysis data
1 parent d2e2374 commit 8c3a224

File tree

6 files changed

+542
-0
lines changed

6 files changed

+542
-0
lines changed

src/server-factory.ts

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

2122
/**
2223
* Wrapper class for BrowserStack MCP Server
@@ -55,6 +56,7 @@ export class BrowserStackMcpServer {
5556
addFailureLogsTools,
5657
addAutomateTools,
5758
addSelfHealTools,
59+
addRCATools,
5860
];
5961

6062
toolAdders.forEach((adder) => {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 name first with smaller heading
11+
output += `### ${testCase.displayName || `Test Case ${index + 1}`}\n`;
12+
output += `**Test ID:** ${testCase.id}\n`;
13+
output += `**Status:** ${testCase.state}\n\n`;
14+
15+
if (testCase.rcaData?.originalResponse?.rcaData) {
16+
const rca = testCase.rcaData.originalResponse.rcaData;
17+
18+
if (rca.root_cause) {
19+
output += `**Root Cause:** ${rca.root_cause}\n\n`;
20+
}
21+
22+
if (rca.description) {
23+
output += `**Description:**\n${rca.description}\n\n`;
24+
}
25+
26+
if (rca.possible_fix) {
27+
output += `**Recommended Fix:**\n${rca.possible_fix}\n\n`;
28+
}
29+
}
30+
31+
output += "---\n\n";
32+
});
33+
34+
return output;
35+
}
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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
interface TestDetails {
2+
status: string;
3+
details: any;
4+
children?: TestDetails[];
5+
display_name?: string;
6+
}
7+
8+
interface TestRun {
9+
hierarchy: TestDetails[];
10+
pagination?: {
11+
has_next: boolean;
12+
next_page: string | null;
13+
};
14+
}
15+
16+
export interface FailedTestInfo {
17+
id: string;
18+
displayName: string;
19+
}
20+
21+
export async function getFailedTestIds(
22+
buildId: string,
23+
authString: string,
24+
): Promise<FailedTestInfo[]> {
25+
const baseUrl = `https://api-automation.browserstack.com/ext/v1/builds/${buildId}/testRuns?test_statuses=failed`;
26+
let nextUrl = baseUrl;
27+
let allFailedTests: FailedTestInfo[] = [];
28+
let requestNumber = 0;
29+
30+
// Construct Basic auth header
31+
const encodedCredentials = Buffer.from(authString).toString("base64");
32+
const authHeader = `Basic ${encodedCredentials}`;
33+
34+
try {
35+
while (true) {
36+
requestNumber++;
37+
38+
const response = await fetch(nextUrl, {
39+
headers: {
40+
Authorization: authHeader,
41+
"Content-Type": "application/json",
42+
},
43+
});
44+
45+
if (!response.ok) {
46+
throw new Error(
47+
`Failed to fetch test runs: ${response.status} ${response.statusText}`,
48+
);
49+
}
50+
51+
const data = (await response.json()) as TestRun;
52+
53+
// Extract failed IDs from current page
54+
if (data.hierarchy && data.hierarchy.length > 0) {
55+
const currentFailedTests = extractFailedTestIds(data.hierarchy);
56+
allFailedTests = allFailedTests.concat(currentFailedTests);
57+
}
58+
59+
// Check for pagination termination conditions
60+
if (!data.pagination?.has_next || !data.pagination.next_page) {
61+
break;
62+
}
63+
64+
// Safety limit to prevent runaway requests
65+
if (requestNumber >= 5) {
66+
break;
67+
}
68+
69+
// Prepare next request
70+
nextUrl = `${baseUrl}?next_page=${encodeURIComponent(data.pagination.next_page)}`;
71+
}
72+
73+
// Return unique failed test IDs
74+
return allFailedTests;
75+
} catch (error) {
76+
console.error("Error fetching failed tests:", error);
77+
throw error;
78+
}
79+
}
80+
81+
// Recursive function to extract failed test IDs from hierarchy
82+
function extractFailedTestIds(hierarchy: TestDetails[]): FailedTestInfo[] {
83+
let failedTests: FailedTestInfo[] = [];
84+
85+
for (const node of hierarchy) {
86+
if (node.details?.status === "failed" && node.details?.run_count) {
87+
if (node.details?.observability_url) {
88+
const idMatch = node.details.observability_url.match(/details=(\d+)/);
89+
if (idMatch) {
90+
failedTests.push({
91+
id: idMatch[1],
92+
displayName: node.display_name || `Test ${idMatch[1]}`
93+
});
94+
}
95+
}
96+
}
97+
98+
if (node.children && node.children.length > 0) {
99+
failedTests = failedTests.concat(extractFailedTestIds(node.children));
100+
}
101+
}
102+
103+
return failedTests;
104+
}

0 commit comments

Comments
 (0)