Skip to content

Commit bb7e639

Browse files
committed
Restoring files
1 parent 33c66ec commit bb7e639

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed

src/tools/observability.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { z } from "zod";
3+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4+
import { getLatestO11YBuildInfo } from "../lib/api.js";
5+
import { trackMCP } from "../lib/instrumentation.js";
6+
import logger from "../logger.js";
7+
8+
export async function getFailuresInLastRun(
9+
buildName: string,
10+
projectName: string,
11+
): Promise<CallToolResult> {
12+
const buildsData = await getLatestO11YBuildInfo(buildName, projectName);
13+
14+
const observabilityUrl = buildsData.observability_url;
15+
if (!observabilityUrl) {
16+
throw new Error(
17+
"No observability URL found in build data, this is likely because the build is not yet available on BrowserStack Observability.",
18+
);
19+
}
20+
21+
let overview = "No overview available";
22+
if (buildsData.unique_errors?.overview?.insight) {
23+
overview = buildsData.unique_errors.overview.insight;
24+
}
25+
26+
let details = "No error details available";
27+
if (buildsData.unique_errors?.top_unique_errors?.length > 0) {
28+
details = buildsData.unique_errors.top_unique_errors
29+
.map((error: any) => error.error)
30+
.filter(Boolean)
31+
.join("\n");
32+
}
33+
34+
return {
35+
content: [
36+
{
37+
type: "text",
38+
text: `Observability URL: ${observabilityUrl}\nOverview: ${overview}\nError Details: ${details}`,
39+
},
40+
],
41+
};
42+
}
43+
44+
export default function addObservabilityTools(server: McpServer) {
45+
server.tool(
46+
"getFailuresInLastRun",
47+
"Use this tool to debug failures in the last run of the test suite on BrowserStack. Use only when browserstack.yml file is present in the project root.",
48+
{
49+
buildName: z
50+
.string()
51+
.describe(
52+
"Name of the build to get failures for. This is the 'build' key in the browserstack.yml file. If not sure, ask the user for the build name.",
53+
),
54+
projectName: z
55+
.string()
56+
.describe(
57+
"Name of the project to get failures for. This is the 'projectName' key in the browserstack.yml file. If not sure, ask the user for the project name.",
58+
),
59+
},
60+
async (args) => {
61+
try {
62+
trackMCP("getFailuresInLastRun", server.server.getClientVersion()!);
63+
return await getFailuresInLastRun(args.buildName, args.projectName);
64+
} catch (error) {
65+
logger.error("Failed to get failures in the last run: %s", error);
66+
trackMCP(
67+
"getFailuresInLastRun",
68+
server.server.getClientVersion()!,
69+
error,
70+
);
71+
return {
72+
content: [
73+
{
74+
type: "text",
75+
text: `Failed to get failures in the last run. Error: ${error}. Please open an issue on GitHub if this is an issue with BrowserStack`,
76+
isError: true,
77+
},
78+
],
79+
isError: true,
80+
};
81+
}
82+
},
83+
);
84+
}

tests/tools/observability.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { getFailuresInLastRun } from '../../src/tools/observability';
2+
import { getLatestO11YBuildInfo } from '../../src/lib/api';
3+
import { beforeEach, it, expect, describe, vi, Mock } from 'vitest'
4+
5+
// Mock the API module
6+
vi.mock('../../src/lib/api', () => ({
7+
getLatestO11YBuildInfo: vi.fn(),
8+
}));
9+
10+
vi.mock('../../src/lib/instrumentation', () => ({
11+
trackMCP: vi.fn()
12+
}));
13+
14+
describe('getFailuresInLastRun', () => {
15+
beforeEach(() => {
16+
vi.clearAllMocks();
17+
});
18+
19+
const validBuildData = {
20+
observability_url: 'https://observability.browserstack.com/123',
21+
unique_errors: {
22+
overview: {
23+
insight: 'Test insight message'
24+
},
25+
top_unique_errors: [
26+
{ error: 'Error 1' },
27+
{ error: 'Error 2' }
28+
]
29+
}
30+
};
31+
32+
it('should successfully retrieve failures for a valid build', async () => {
33+
(getLatestO11YBuildInfo as Mock).mockResolvedValue(validBuildData);
34+
35+
const result = await getFailuresInLastRun('test-build', 'test-project');
36+
37+
expect(getLatestO11YBuildInfo).toHaveBeenCalledWith('test-build', 'test-project');
38+
expect(result.content[0].text).toContain('https://observability.browserstack.com/123');
39+
expect(result.content[0].text).toContain('Test insight message');
40+
expect(result.content[0].text).toContain('Error 1');
41+
expect(result.content[0].text).toContain('Error 2');
42+
});
43+
44+
it('should handle missing observability URL', async () => {
45+
(getLatestO11YBuildInfo as Mock).mockResolvedValue({
46+
...validBuildData,
47+
observability_url: null
48+
});
49+
50+
await expect(getFailuresInLastRun('test-build', 'test-project'))
51+
.rejects.toThrow('No observability URL found in build data');
52+
});
53+
54+
it('should handle missing overview insight', async () => {
55+
(getLatestO11YBuildInfo as Mock).mockResolvedValue({
56+
...validBuildData,
57+
unique_errors: {
58+
...validBuildData.unique_errors,
59+
overview: {}
60+
}
61+
});
62+
63+
const result = await getFailuresInLastRun('test-build', 'test-project');
64+
expect(result.content[0].text).toContain('No overview available');
65+
});
66+
67+
it('should handle missing error details', async () => {
68+
(getLatestO11YBuildInfo as Mock).mockResolvedValue({
69+
...validBuildData,
70+
unique_errors: {
71+
...validBuildData.unique_errors,
72+
top_unique_errors: []
73+
}
74+
});
75+
76+
const result = await getFailuresInLastRun('test-build', 'test-project');
77+
expect(result.content[0].text).toContain('No error details available');
78+
});
79+
80+
it('should handle API errors', async () => {
81+
(getLatestO11YBuildInfo as Mock).mockRejectedValue(new Error('API Error'));
82+
83+
await expect(getFailuresInLastRun('test-build', 'test-project'))
84+
.rejects.toThrow('API Error');
85+
});
86+
87+
it('should handle empty build data', async () => {
88+
(getLatestO11YBuildInfo as Mock).mockResolvedValue({});
89+
90+
await expect(getFailuresInLastRun('test-build', 'test-project'))
91+
.rejects.toThrow('No observability URL found in build data');
92+
});
93+
94+
it('should handle partial build data', async () => {
95+
(getLatestO11YBuildInfo as Mock).mockResolvedValue({
96+
observability_url: 'https://observability.browserstack.com/123',
97+
unique_errors: {}
98+
});
99+
100+
const result = await getFailuresInLastRun('test-build', 'test-project');
101+
expect(result.content[0].text).toContain('No overview available');
102+
expect(result.content[0].text).toContain('No error details available');
103+
});
104+
});

0 commit comments

Comments
 (0)