Skip to content

Commit 9ae27d2

Browse files
authored
feat: unified failure logs utility for automate and app-automate (#35)
* Update WebDriverIO import style and clean up unused interface * feat: unified failure logs utility for automate and app-automate * Adding Tests for the failure logs * Modified according to the module pattern
1 parent 89a94e4 commit 9ae27d2

File tree

9 files changed

+845
-227
lines changed

9 files changed

+845
-227
lines changed

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import addAppLiveTools from "./tools/applive.js";
1010
import addObservabilityTools from "./tools/observability.js";
1111
import addBrowserLiveTools from "./tools/live.js";
1212
import addAccessibilityTools from "./tools/accessibility.js";
13-
import addAutomateTools from "./tools/automate.js";
1413
import addTestManagementTools from "./tools/testmanagement.js";
1514
import addAppAutomationTools from "./tools/appautomate.js";
15+
import addFailureLogsTools from "./tools/getFailureLogs.js";
1616
import { trackMCP } from "./lib/instrumentation.js";
1717

1818
function registerTools(server: McpServer) {
@@ -21,9 +21,9 @@ function registerTools(server: McpServer) {
2121
addBrowserLiveTools(server);
2222
addObservabilityTools(server);
2323
addAccessibilityTools(server);
24-
addAutomateTools(server);
2524
addTestManagementTools(server);
2625
addAppAutomationTools(server);
26+
addFailureLogsTools(server);
2727
}
2828

2929
// Create an MCP server

src/lib/api.ts

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import config from "../config.js";
2-
import { HarEntry, HarFile } from "./utils.js";
32

43
export async function getLatestO11YBuildInfo(
54
buildName: string,
@@ -28,62 +27,3 @@ export async function getLatestO11YBuildInfo(
2827

2928
return buildsResponse.json();
3029
}
31-
32-
// Fetches network logs for a given session ID and returns only failure logs
33-
export async function retrieveNetworkFailures(sessionId: string): Promise<any> {
34-
if (!sessionId) {
35-
throw new Error("Session ID is required");
36-
}
37-
const url = `https://api.browserstack.com/automate/sessions/${sessionId}/networklogs`;
38-
const auth = Buffer.from(
39-
`${config.browserstackUsername}:${config.browserstackAccessKey}`,
40-
).toString("base64");
41-
42-
const response = await fetch(url, {
43-
method: "GET",
44-
headers: {
45-
"Content-Type": "application/json",
46-
Authorization: `Basic ${auth}`,
47-
},
48-
});
49-
50-
if (!response.ok) {
51-
if (response.status === 404) {
52-
throw new Error("Invalid session ID");
53-
}
54-
throw new Error(`Failed to fetch network logs: ${response.statusText}`);
55-
}
56-
57-
const networklogs: HarFile = await response.json();
58-
59-
// Filter for failure logs
60-
const failureEntries: HarEntry[] = networklogs.log.entries.filter(
61-
(entry: HarEntry) => {
62-
return (
63-
entry.response.status === 0 ||
64-
entry.response.status >= 400 ||
65-
entry.response._error !== undefined
66-
);
67-
},
68-
);
69-
70-
// Return only the failure entries with some context
71-
return {
72-
failures: failureEntries.map((entry: any) => ({
73-
startedDateTime: entry.startedDateTime,
74-
request: {
75-
method: entry.request?.method,
76-
url: entry.request?.url,
77-
queryString: entry.request?.queryString,
78-
},
79-
response: {
80-
status: entry.response?.status,
81-
statusText: entry.response?.statusText,
82-
_error: entry.response?._error,
83-
},
84-
serverIPAddress: entry.serverIPAddress,
85-
time: entry.time,
86-
})),
87-
totalFailures: failureEntries.length,
88-
};
89-
}

src/lib/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,26 @@ export async function maybeCompressBase64(base64: string): Promise {
4545

4646
return compressedBuffer.toString("base64");
4747
}
48+
49+
export async function assertOkResponse(response: Response, action: string) {
50+
if (!response.ok) {
51+
if (response.status === 404) {
52+
throw new Error(`Invalid session ID for ${action}`);
53+
}
54+
throw new Error(
55+
`Failed to fetch logs for ${action}: ${response.statusText}`,
56+
);
57+
}
58+
}
59+
60+
export function filterLinesByKeywords(
61+
logText: string,
62+
keywords: string[],
63+
): string[] {
64+
return logText
65+
.split(/\r?\n/)
66+
.map((line) => line.trim())
67+
.filter((line) =>
68+
keywords.some((keyword) => line.toLowerCase().includes(keyword)),
69+
);
70+
}

src/tools/automate.ts

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import config from "../../config.js";
2+
import { assertOkResponse, filterLinesByKeywords } from "../../lib/utils.js";
3+
4+
const auth = Buffer.from(
5+
`${config.browserstackUsername}:${config.browserstackAccessKey}`,
6+
).toString("base64");
7+
8+
// DEVICE LOGS
9+
export async function retrieveDeviceLogs(
10+
sessionId: string,
11+
buildId: string,
12+
): Promise<string[]> {
13+
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/deviceLogs`;
14+
15+
const response = await fetch(url, {
16+
headers: {
17+
"Content-Type": "application/json",
18+
Authorization: `Basic ${auth}`,
19+
},
20+
});
21+
22+
await assertOkResponse(response, "device logs");
23+
24+
const logText = await response.text();
25+
return filterDeviceFailures(logText);
26+
}
27+
28+
// APPIUM LOGS
29+
export async function retrieveAppiumLogs(
30+
sessionId: string,
31+
buildId: string,
32+
): Promise<string[]> {
33+
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/appiumlogs`;
34+
35+
const response = await fetch(url, {
36+
headers: {
37+
"Content-Type": "application/json",
38+
Authorization: `Basic ${auth}`,
39+
},
40+
});
41+
42+
await assertOkResponse(response, "Appium logs");
43+
44+
const logText = await response.text();
45+
return filterAppiumFailures(logText);
46+
}
47+
48+
// CRASH LOGS
49+
export async function retrieveCrashLogs(
50+
sessionId: string,
51+
buildId: string,
52+
): Promise<string[]> {
53+
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/crashlogs`;
54+
55+
const response = await fetch(url, {
56+
headers: {
57+
"Content-Type": "application/json",
58+
Authorization: `Basic ${auth}`,
59+
},
60+
});
61+
62+
await assertOkResponse(response, "crash logs");
63+
64+
const logText = await response.text();
65+
return filterCrashFailures(logText);
66+
}
67+
68+
// FILTER HELPERS
69+
70+
export function filterDeviceFailures(logText: string): string[] {
71+
const keywords = [
72+
"error",
73+
"exception",
74+
"fatal",
75+
"anr",
76+
"not responding",
77+
"process crashed",
78+
"crash",
79+
"force close",
80+
"signal",
81+
"java.lang.",
82+
"unable to",
83+
];
84+
return filterLinesByKeywords(logText, keywords);
85+
}
86+
87+
export function filterAppiumFailures(logText: string): string[] {
88+
const keywords = [
89+
"error",
90+
"fail",
91+
"exception",
92+
"not found",
93+
"no such element",
94+
"unable to",
95+
"stacktrace",
96+
"appium exited",
97+
"command failed",
98+
"invalid selector",
99+
];
100+
return filterLinesByKeywords(logText, keywords);
101+
}
102+
103+
export function filterCrashFailures(logText: string): string[] {
104+
const keywords = [
105+
"fatal exception",
106+
"crash",
107+
"signal",
108+
"java.lang.",
109+
"caused by:",
110+
"native crash",
111+
"anr",
112+
"abort message",
113+
"application has stopped unexpectedly",
114+
];
115+
return filterLinesByKeywords(logText, keywords);
116+
}

0 commit comments

Comments
 (0)