From 2e0d78b84fdc3b62414be6fd0e08457dc5009e7e Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 28 May 2025 23:28:00 +0530 Subject: [PATCH 1/6] feat : O11Y Failure Logs --- package.json | 1 + src/index.ts | 2 + src/lib/constants.ts | 16 + src/tools/analyse-test-failure.ts | 68 ++++ src/tools/failurelogs-utils/automate.ts | 40 ++ src/tools/testfailurelogs-utils/filters.ts | 371 +++++++++++++++++++ src/tools/testfailurelogs-utils/o11y-logs.ts | 66 ++++ src/tools/testfailurelogs-utils/test-case.ts | 44 +++ src/tools/testfailurelogs-utils/types.ts | 27 ++ src/tools/testfailurelogs-utils/utils.ts | 156 ++++++++ 10 files changed, 791 insertions(+) create mode 100644 src/tools/analyse-test-failure.ts create mode 100644 src/tools/testfailurelogs-utils/filters.ts create mode 100644 src/tools/testfailurelogs-utils/o11y-logs.ts create mode 100644 src/tools/testfailurelogs-utils/test-case.ts create mode 100644 src/tools/testfailurelogs-utils/types.ts create mode 100644 src/tools/testfailurelogs-utils/utils.ts diff --git a/package.json b/package.json index f2ad847..ccb15f3 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "axios": "^1.8.4", "browserstack-local": "^1.5.6", "csv-parse": "^5.6.0", + "date-fns": "^4.1.0", "dotenv": "^16.5.0", "form-data": "^4.0.2", "pino": "^9.6.0", diff --git a/src/index.ts b/src/index.ts index bfad2f9..68e460c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import addAppAutomationTools from "./tools/appautomate.js"; import addFailureLogsTools from "./tools/getFailureLogs.js"; import addAutomateTools from "./tools/automate.js"; import addSelfHealTools from "./tools/selfheal.js"; +import addAnalyseTestFailureTools from "./tools/analyse-test-failure.js"; import { setupOnInitialized } from "./oninitialized.js"; function registerTools(server: McpServer) { @@ -28,6 +29,7 @@ function registerTools(server: McpServer) { addFailureLogsTools(server); addAutomateTools(server); addSelfHealTools(server); + addAnalyseTestFailureTools(server); } // Create an MCP server diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 01fe078..12ce0c0 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -15,6 +15,22 @@ export const AppAutomateLogType = { CrashLogs: "crashLogs", } as const; +export const BrowserstackLogTypes = { + Network: "network", + Session: "session", + Text: "text", + Console: "console", + Selenium: "selenium", + Appium: "appium", + Device: "device", + Crash: "crash", + Playwright: "playwright", + Telemetry: "telemetry", + Performance: "performance", + Terminal: "terminal", + BrowserProfiling: "browserProfiling", +} as const; + export type SessionType = (typeof SessionType)[keyof typeof SessionType]; export type AutomateLogType = (typeof AutomateLogType)[keyof typeof AutomateLogType]; diff --git a/src/tools/analyse-test-failure.ts b/src/tools/analyse-test-failure.ts new file mode 100644 index 0000000..45ca431 --- /dev/null +++ b/src/tools/analyse-test-failure.ts @@ -0,0 +1,68 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +import { retrieveTestObservabilityLogs } from "./testfailurelogs-utils/o11y-logs.js"; +import { retrieveObservabilityTestCase } from "./testfailurelogs-utils/test-case.js"; + +export async function analyseTestFailure(args: { + testId: string; +}): Promise { + try { + const failureLogs = await retrieveTestObservabilityLogs(args.testId); + const testCase = await retrieveObservabilityTestCase(args.testId); + + const response = { + failure_logs: failureLogs, + test_case: testCase, + }; + + return { + content: [ + { + type: "text", + text: JSON.stringify(response, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text", + text: `Error reading log files: ${errorMessage}`, + }, + ], + }; + } +} + +export default function addAnalyseTestFailureTool(server: McpServer) { + server.tool( + "analyseTestFailure", + "Fetch the logs of a test run from BrowserStack", + { + testId: z + .string() + .describe("The BrowserStack test ID to fetch logs from"), + }, + async (args) => { + try { + return await analyseTestFailure(args); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text", + text: `Error during fetching test logs: ${errorMessage}`, + }, + ], + }; + } + }, + ); +} diff --git a/src/tools/failurelogs-utils/automate.ts b/src/tools/failurelogs-utils/automate.ts index e8b5e95..e9c1c2c 100644 --- a/src/tools/failurelogs-utils/automate.ts +++ b/src/tools/failurelogs-utils/automate.ts @@ -138,3 +138,43 @@ export function filterConsoleFailures(logText: string): string[] { ]; return filterLinesByKeywords(logText, keywords); } + +export function filterSDKFailures(logText: string): string[] { + const keywords = [ + "fail", + "error", + "exception", + "fatal", + "unable to", + "not found", + "timeout", + "crash", + "rejected", + "denied", + "broken", + "unsuccessful", + "panic", + ]; + return filterLinesByKeywords(logText, keywords); +} + +export function filterHookRunFailures(logText: string): string[] { + const keywords = [ + "except block error", + "unable to", + "cannot find module", + "doesn't exist", + "error while loading", + "error:", + "failed to", + "not found", + "missing", + "rejected", + "no such file", + "npm ERR!", + "stderr", + "fatal", + ]; + + return filterLinesByKeywords(logText, keywords); +} diff --git a/src/tools/testfailurelogs-utils/filters.ts b/src/tools/testfailurelogs-utils/filters.ts new file mode 100644 index 0000000..a7eb70e --- /dev/null +++ b/src/tools/testfailurelogs-utils/filters.ts @@ -0,0 +1,371 @@ +import logger from "../../logger.js"; +import { + HarEntry, + HarFile, + validateLogResponse, +} from "../failurelogs-utils/utils.js"; +import { + filterConsoleFailures, + filterSessionFailures, +} from "../failurelogs-utils/automate.js"; +import { + filterAppiumFailures, + filterCrashFailures, + filterDeviceFailures, +} from "../failurelogs-utils/app-automate.js"; +import { + filterSDKFailures, + filterHookRunFailures, +} from "../failurelogs-utils/automate.js"; +import { + filterLogsByTimestamp, + filterLogsByTimestampHook, + filterLogsByTimestampSDK, + filterLogsByTimestampConsole, + filterLogsByTimestampSelenium, + filterLogsByTimestampDevice, + filterLogsByTimestampAppium, +} from "./utils.js"; +import { BrowserstackLogTypes } from "../../lib/constants.js"; + +// Main log filter for BrowserStack logs +export async function filterBrowserstackLogs( + browserstackLogs: any, + testStartedAt?: string, + testFinishedAt?: string, +): Promise { + if (!browserstackLogs) return {}; + + const result: Record = {}; + const startTime = testStartedAt || null; + const endTime = testFinishedAt || null; + + for (const logType in browserstackLogs) { + try { + const logData = browserstackLogs[logType]; + let finalLogContent; + + switch (logType) { + case BrowserstackLogTypes.Text: + if (logData.url) { + finalLogContent = await validateAndFilterTextLogs( + logData.url, + startTime, + endTime, + ); + } + break; + case BrowserstackLogTypes.Network: + if (logData.url) { + finalLogContent = await validateAndFilterNetworkLogs( + logData.url, + startTime, + endTime, + ); + } + break; + case BrowserstackLogTypes.Selenium: + if (logData.url) { + finalLogContent = await validateAndFilterSeleniumLogs( + logData.url, + startTime, + endTime, + ); + } + break; + case BrowserstackLogTypes.Console: + if (logData.url) { + finalLogContent = await validateAndFilterConsoleLogs( + logData.url, + startTime, + endTime, + ); + } + break; + case BrowserstackLogTypes.Device: + if (logData.url) { + finalLogContent = await validateAndFilterDeviceLogs( + logData.url, + startTime, + endTime, + ); + } + break; + case BrowserstackLogTypes.Appium: + if (logData.url) { + finalLogContent = await validateAndFilterAppiumLogs( + logData.url, + startTime, + endTime, + ); + } + break; + } + result[logType] = finalLogContent; + } catch (error) { + logger.error( + `Failed to fetch ${logType} logs: ${error instanceof Error ? error.message : String(error)}`, + ); + result[logType] = null; + } + } + return result; +} + +// Filter SDK logs by timestamp +export async function filterSdkLogs( + logUrls: string[], + testStartedAt?: string, + testFinishedAt?: string, +): Promise { + if (!testStartedAt || !testFinishedAt) return logUrls; + + const filteredLogs: string[] = []; + + for (const logUrl of logUrls) { + try { + const logContent = await fetch(logUrl).then((res) => res.text()); + const filteredLines = filterLogsByTimestampSDK( + logContent, + testStartedAt, + testFinishedAt, + ); + filteredLogs.push(filteredLines.join("\n")); + } catch (error) { + logger.error( + `Failed to filter SDK log: ${error instanceof Error ? error.message : String(error)}`, + ); + filteredLogs.push(`Error fetching or filtering log at ${logUrl}`); + } + } + const logs = filterSDKFailures(filteredLogs.join("\n")); + return logs.length > 0 + ? `Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}` + : "No failures found"; +} + +// Filter hook run logs by timestamp +export async function filterHookRunLogs( + hookRunLogs: string[], + testStartedAt?: string, + testFinishedAt?: string, +) { + if (!testStartedAt || !testFinishedAt) return hookRunLogs; + + const filteredLogs: string[] = []; + + for (const log of hookRunLogs) { + try { + const logContent = await fetch(log).then((res) => res.text()); + const filteredLines = filterLogsByTimestampHook( + logContent, + testStartedAt, + testFinishedAt, + ); + filteredLogs.push(filteredLines.join("\n")); + } catch (error) { + logger.error(`Failed to filter hook run log: ${error}`); + filteredLogs.push(`Error fetching or filtering log at ${log}`); + } + } + const logs = filterHookRunFailures(filteredLogs.join("\n")); + return logs.length > 0 + ? `Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}` + : "No failures found"; +} + +// Text log validation and filtering +export async function validateAndFilterTextLogs( + url: string, + startTime: string | null = null, + endTime: string | null = null, +) { + try { + const response = await fetch(url); + const validationError = validateLogResponse(response, "text logs"); + if (validationError) return validationError.message!; + + const logText = await response.text(); + const logLines = logText.split("\n"); + const filteredLines = + startTime && endTime + ? filterLogsByTimestamp(logLines, startTime, endTime) + : logLines; + const logs = filterSessionFailures(filteredLines.join("\n")); + return logs.length > 0 + ? `Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}` + : "No failures found"; + } catch (error) { + logger.error( + `Error processing text logs: ${error instanceof Error ? error.message : String(error)}`, + ); + throw error; + } +} + +// Network log validation and filtering +export async function validateAndFilterNetworkLogs( + url: string, + startTime: string | null = null, + endTime: string | null = null, +): Promise { + const response = await fetch(url); + const validationError = validateLogResponse(response, "network logs"); + if (validationError) return validationError.message!; + + const networklogs: HarFile = await response.json(); + const startDate = startTime ? new Date(startTime) : null; + const endDate = endTime ? new Date(endTime) : null; + + const failureEntries: HarEntry[] = networklogs.log.entries.filter( + (entry: HarEntry) => { + if (startDate && endDate) { + const entryTime = new Date(entry.startedDateTime).getTime(); + return ( + entryTime >= startDate.getTime() && + entryTime <= endDate.getTime() && + (entry.response.status === 0 || + entry.response.status >= 400 || + entry.response._error !== undefined) + ); + } + return ( + entry.response.status === 0 || + entry.response.status >= 400 || + entry.response._error !== undefined + ); + }, + ); + + return failureEntries.length > 0 + ? `Network Failures (${failureEntries.length} found):\n${JSON.stringify( + failureEntries.map((entry: any) => ({ + startedDateTime: entry.startedDateTime, + request: { + method: entry.request?.method, + url: entry.request?.url, + queryString: entry.request?.queryString, + }, + response: { + status: entry.response?.status, + statusText: entry.response?.statusText, + _error: entry.response?._error, + }, + serverIPAddress: entry.serverIPAddress, + time: entry.time, + })), + null, + 2, + )}` + : "No network failures found"; +} + +// Console log validation and filtering +export async function validateAndFilterConsoleLogs( + url: string, + startTime: string | null = null, + endTime: string | null = null, +): Promise { + const response = await fetch(url); + const validationError = validateLogResponse(response, "console logs"); + if (validationError) return validationError.message!; + + const logText = await response.text(); + const filteredLines = + startTime && endTime + ? filterLogsByTimestampConsole(logText, startTime, endTime) + : logText.split("\n").filter((line) => line.trim()); + const logs = filterConsoleFailures(filteredLines.join("\n")); + + return logs.length > 0 + ? `Console Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}` + : "No console failures found"; +} + +// Device log validation and filtering +export async function validateAndFilterDeviceLogs( + url: string, + startTime: string | null = null, + endTime: string | null = null, +): Promise { + const response = await fetch(url); + const validationError = validateLogResponse(response, "device logs"); + if (validationError) return validationError.message!; + + const logText = await response.text(); + const filteredLines = + startTime && endTime + ? filterLogsByTimestampDevice(logText, startTime, endTime) + : logText.split("\n").filter((line) => line.trim()); + const logs = filterDeviceFailures(filteredLines.join("\n")); + + return logs.length > 0 + ? `Device Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}` + : "No device failures found"; +} + +// Appium log validation and filtering +export async function validateAndFilterAppiumLogs( + url: string, + startTime: string | null = null, + endTime: string | null = null, +): Promise { + const response = await fetch(url); + const validationError = validateLogResponse(response, "Appium logs"); + if (validationError) return validationError.message!; + + const logText = await response.text(); + const filteredLines = + startTime && endTime + ? filterLogsByTimestampAppium(logText, startTime, endTime) + : logText.split("\n").filter((line) => line.trim()); + const logs = filterAppiumFailures(filteredLines.join("\n")); + + return logs.length > 0 + ? `Appium Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}` + : "No Appium failures found"; +} + +// Crash log validation and filtering +export async function validateAndFilterCrashLogs( + url: string, + startTime: string | null = null, + endTime: string | null = null, +): Promise { + const response = await fetch(url); + const validationError = validateLogResponse(response, "crash logs"); + if (validationError) return validationError.message!; + + const logText = await response.text(); + const logLines = logText.split("\n"); + const filteredLines = + startTime && endTime + ? filterLogsByTimestamp(logLines, startTime, endTime) + : logLines; + const logs = filterCrashFailures(filteredLines.join("\n")); + + return logs.length > 0 + ? `Crash Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}` + : "No crash failures found"; +} + +// Selenium log validation and filtering +export async function validateAndFilterSeleniumLogs( + url: string, + startTime: string | null = null, + endTime: string | null = null, +): Promise { + const response = await fetch(url); + const validationError = validateLogResponse(response, "Selenium logs"); + if (validationError) return validationError.message!; + + const logText = await response.text(); + const filteredLines = + startTime && endTime + ? filterLogsByTimestampSelenium(logText, startTime, endTime) + : logText.split("\n").filter((line) => line.trim()); + + return filteredLines.length > 0 + ? `Selenium Failures (${filteredLines.length} found):\n${JSON.stringify(filteredLines, null, 2)}` + : "No Selenium failures found"; +} diff --git a/src/tools/testfailurelogs-utils/o11y-logs.ts b/src/tools/testfailurelogs-utils/o11y-logs.ts new file mode 100644 index 0000000..a2050f7 --- /dev/null +++ b/src/tools/testfailurelogs-utils/o11y-logs.ts @@ -0,0 +1,66 @@ +import { TestObservabilityLog, TestObservabilityLogResponse } from "./types.js"; +import config from "../../config.js"; +import { + filterBrowserstackLogs, + filterHookRunLogs, + filterSdkLogs, +} from "./filters.js"; + +// Authentication +const auth = Buffer.from( + `${config.browserstackUsername}:${config.browserstackAccessKey}`, +).toString("base64"); + +// Fetch and filter observability logs for a test run +export async function retrieveTestObservabilityLogs( + testId: string, +): Promise { + const url = `https://api-observability.browserstack.com/ext/v1/testRun/${testId}/logs`; + + try { + const response = await fetch(url, { + headers: { + "Content-Type": "application/json", + Authorization: `Basic ${auth}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to retrieve logs: ${response.status} ${response.statusText} - ${errorText}`, + ); + } + + const ollyLogs = (await response.json()) as TestObservabilityLogResponse; + return await processAndFilterLogs(ollyLogs); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to retrieve observability logs: ${message}`); + } +} + +// Filter and structure logs for output +async function processAndFilterLogs( + logData: TestObservabilityLogResponse, +): Promise { + return { + failureLogs: logData.failureLogs, + browserstackLogs: await filterBrowserstackLogs( + logData.browserstackLogs, + logData.testStartedAt, + logData.testFinishedAt, + ), + sdkLogs: await filterSdkLogs( + logData.sdkLogs, + logData.testStartedAt, + logData.testFinishedAt, + ), + hookRunLogs: await filterHookRunLogs( + logData.hookRunLogs, + logData.testStartedAt, + logData.testFinishedAt, + ), + framework: logData.framework || null, + }; +} diff --git a/src/tools/testfailurelogs-utils/test-case.ts b/src/tools/testfailurelogs-utils/test-case.ts new file mode 100644 index 0000000..54c4295 --- /dev/null +++ b/src/tools/testfailurelogs-utils/test-case.ts @@ -0,0 +1,44 @@ +import logger from "../../logger.js"; +import { ObservabilityTestDetails } from "./types.js"; +import config from "../../config.js"; + +// Authentication +const auth = Buffer.from( + `${config.browserstackUsername}:${config.browserstackAccessKey}`, +).toString("base64"); + +export async function retrieveObservabilityTestCase( + testId: string, +): Promise { + logger.info(`Retrieving test case for test ID: ${testId}`); + + try { + const url = `https://api-observability.browserstack.com/ext/v1/testRun/${testId}/details`; + + const response = await fetch(url, { + headers: { + "Content-Type": "application/json", + Authorization: `Basic ${auth}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to retrieve test case: ${response.status} ${response.statusText} - ${errorText}`, + ); + } + + const ollyTestDetails = (await response.json()) as ObservabilityTestDetails; + + if (!ollyTestDetails.testCode || !ollyTestDetails.testCode.code) { + return "No test code found in the response"; + } + + return ollyTestDetails.testCode.code; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Failed to retrieve test case: ${errorMessage}`); + throw new Error(`Failed to retrieve test case: ${errorMessage}`); + } +} diff --git a/src/tools/testfailurelogs-utils/types.ts b/src/tools/testfailurelogs-utils/types.ts new file mode 100644 index 0000000..4c8dd74 --- /dev/null +++ b/src/tools/testfailurelogs-utils/types.ts @@ -0,0 +1,27 @@ +//Interface for test observability logs +export interface TestObservabilityLog { + sdkLogs: string[]; + failureLogs: any[]; + browserstackLogs?: any | null; + hookRunLogs: string[]; + framework?: string | null; +} + +//Interface for test details +export interface ObservabilityTestDetails { + testCode: any; + testMetadata: any; +} + +//Interface for log response structure +export interface TestObservabilityLogResponse { + testStartedAt?: string; + testFinishedAt?: string; + browserstackLogs?: any | null; + sdkLogs: string[]; + hookRunLogs: string[]; + failureLogs: any[]; + stepLogs: any[]; + framework?: string | null; + sessionId?: string | null; +} diff --git a/src/tools/testfailurelogs-utils/utils.ts b/src/tools/testfailurelogs-utils/utils.ts new file mode 100644 index 0000000..57640f5 --- /dev/null +++ b/src/tools/testfailurelogs-utils/utils.ts @@ -0,0 +1,156 @@ +import { parseISO } from "date-fns"; + +// Filters log lines by timestamp range (YYYY-MM-DD HH:mm:ss:SSS) +export function filterLogsByTimestamp( + logs: string[], + startTime: string | null, + endTime: string | null, +): string[] { + if (!startTime || !endTime) return []; + + const startDate = parseISO(startTime); + const endDate = parseISO(endTime); + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return []; + + return logs.filter((line) => { + const match = line.match( + /^(\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2}:\d{2}):(\d{1,3})/, + ); + if (!match) return false; + + const [year, month, day, hour, minute, second] = match[1] + .split(/[- :]/) + .map(Number); + const ms = parseInt(match[2].padStart(3, "0")); + const logTime = new Date( + Date.UTC(year, month - 1, day, hour, minute, second, ms), + ); + return logTime >= startDate && logTime <= endDate; + }); +} + +// Filters SDK log lines by timestamp (expects JSON lines with .timestamp) +export function filterLogsByTimestampSDK( + logContent: string, + testStartedAt: string, + testFinishedAt: string, +): string[] { + const startTime = new Date(testStartedAt); + const endTime = new Date(testFinishedAt); + const logLines = logContent.split("\n").filter((line) => line.trim()); + + return logLines.filter((line) => { + try { + const parsed = JSON.parse(line); + const logTime = new Date(parsed.timestamp); + return logTime >= startTime && logTime <= endTime; + } catch { + return false; + } + }); +} + +// Filters hook run log lines by timestamp (expects JSON lines with .timestamp) +export function filterLogsByTimestampHook( + logContent: string, + testStartedAt: string, + testFinishedAt: string, +): string[] { + const startTime = new Date(testStartedAt); + const endTime = new Date(testFinishedAt); + const logLines = logContent.split("\n").filter((line) => line.trim()); + + return logLines.filter((line) => { + try { + const parsed = JSON.parse(line); + const logTime = new Date(parsed.timestamp); + return logTime >= startTime && logTime <= endTime; + } catch { + return false; + } + }); +} + +// Filters console log lines by millisecond timestamp (first 13 digits) +export function filterLogsByTimestampConsole( + logContent: string, + testStartedAt: string, + testFinishedAt: string, +): string[] { + const startEpoch = new Date(testStartedAt).getTime(); + const endEpoch = new Date(testFinishedAt).getTime(); + const logLines = logContent.split("\n").filter((line) => line.trim()); + + return logLines.filter((line) => { + const match = line.match(/^(\d{13})/); + if (match) { + const timestamp = parseInt(match[1], 10); + return timestamp >= startEpoch && timestamp <= endEpoch; + } + return false; + }); +} + +// Filters Selenium log lines by time (HH:mm:ss.SSS) +export function filterLogsByTimestampSelenium( + logContent: string, + testStartedAt: string, + testFinishedAt: string, +): string[] { + const formatTimeOnly = (dateStr: string): string => { + const d = new Date(dateStr); + return d.toTimeString().slice(0, 12); + }; + + const start = formatTimeOnly(testStartedAt); + const end = formatTimeOnly(testFinishedAt); + const logLines = logContent.split("\n").filter((line) => line.trim()); + + return logLines.filter((line) => { + const match = line.match(/^(\d{2}:\d{2}:\d{2}\.\d{3})/); + if (!match) return false; + const time = match[1]; + return time >= start && time <= end; + }); +} + +// Filters device log lines by timestamp (MM-DD HH:mm:ss.SSS, assumes year from testStartedAt) +export function filterLogsByTimestampDevice( + logContent: string, + testStartedAt: string, + testFinishedAt: string, +): string[] { + const startTime = new Date(testStartedAt); + const endTime = new Date(testFinishedAt); + const year = startTime.getUTCFullYear(); + const logLines = logContent.split("\n").filter((line) => line.trim()); + + return logLines.filter((line) => { + const match = line.match(/^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})/); + if (!match) return false; + const monthDayTime = match[1]; + const fullTimestampStr = `${year}-${monthDayTime.replace(" ", "T")}Z`; + const logTime = new Date(fullTimestampStr); + return logTime >= startTime && logTime <= endTime; + }); +} + +// Filters Appium log lines by timestamp (YYYY-MM-DD HH:mm:ss:SSS) +export function filterLogsByTimestampAppium( + logContent: string, + testStartedAt: string, + testFinishedAt: string, +): string[] { + const startTime = new Date(testStartedAt); + const endTime = new Date(testFinishedAt); + const logLines = logContent.split("\n").filter((line) => line.trim()); + + return logLines.filter((line) => { + const match = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}):(\d{3})/); + if (!match) return false; + const [, datePart, millisPart] = match; + const isoString = `${datePart.replace(" ", "T")}.${millisPart}Z`; + const logTime = new Date(isoString); + return logTime >= startTime && logTime <= endTime; + }); +} From b958b4ddff2c8478a676ef3af90d808734186b6a Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 30 May 2025 11:43:02 +0530 Subject: [PATCH 2/6] Improve error handling and update tool description in analyseTestFailure --- src/tools/analyse-test-failure.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/tools/analyse-test-failure.ts b/src/tools/analyse-test-failure.ts index 45ca431..25928bd 100644 --- a/src/tools/analyse-test-failure.ts +++ b/src/tools/analyse-test-failure.ts @@ -1,9 +1,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; - +import logger from "../logger.js"; import { retrieveTestObservabilityLogs } from "./testfailurelogs-utils/o11y-logs.js"; import { retrieveObservabilityTestCase } from "./testfailurelogs-utils/test-case.js"; +import { trackMCP } from "../lib/instrumentation.js"; export async function analyseTestFailure(args: { testId: string; @@ -26,39 +27,33 @@ export async function analyseTestFailure(args: { ], }; } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - return { - content: [ - { - type: "text", - text: `Error reading log files: ${errorMessage}`, - }, - ], - }; + logger.error("Error during analysing the test ID", error); + throw error; } } export default function addAnalyseTestFailureTool(server: McpServer) { server.tool( "analyseTestFailure", - "Fetch the logs of a test run from BrowserStack", + "Use this tool to analyse a failed test-id from BrowserStack Test Reporting and Analytics", { testId: z .string() - .describe("The BrowserStack test ID to fetch logs from"), + .describe("The BrowserStack test ID to analyse"), }, async (args) => { try { + trackMCP("analyseTestFailure", server.server.getClientVersion()!); return await analyseTestFailure(args); } catch (error) { + trackMCP("analyseTestFailure", server.server.getClientVersion()!); const errorMessage = error instanceof Error ? error.message : "Unknown error"; return { content: [ { type: "text", - text: `Error during fetching test logs: ${errorMessage}`, + text: `Error during analysing test ID: ${errorMessage}`, }, ], }; From 94c3d2203e1aaa3c3b7eb1c6018eb968c1ab7e7b Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 30 May 2025 12:32:47 +0530 Subject: [PATCH 3/6] Invalid Test ID handling --- src/tools/testfailurelogs-utils/o11y-logs.ts | 31 ++++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/tools/testfailurelogs-utils/o11y-logs.ts b/src/tools/testfailurelogs-utils/o11y-logs.ts index a2050f7..efac88d 100644 --- a/src/tools/testfailurelogs-utils/o11y-logs.ts +++ b/src/tools/testfailurelogs-utils/o11y-logs.ts @@ -17,27 +17,26 @@ export async function retrieveTestObservabilityLogs( ): Promise { const url = `https://api-observability.browserstack.com/ext/v1/testRun/${testId}/logs`; - try { - const response = await fetch(url, { - headers: { - "Content-Type": "application/json", - Authorization: `Basic ${auth}`, - }, - }); + const response = await fetch(url, { + headers: { + "Content-Type": "application/json", + Authorization: `Basic ${auth}`, + }, + }); - if (!response.ok) { - const errorText = await response.text(); + if (!response.ok) { + if (response.status === 404) { throw new Error( - `Failed to retrieve logs: ${response.status} ${response.statusText} - ${errorText}`, + `Test with ID ${testId} not found. Please check the test ID and try again.` ); } - - const ollyLogs = (await response.json()) as TestObservabilityLogResponse; - return await processAndFilterLogs(ollyLogs); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to retrieve observability logs: ${message}`); + throw new Error( + `Failed to retrieve observability logs for test ID ${testId}` + ); } + + const ollyLogs = (await response.json()) as TestObservabilityLogResponse; + return await processAndFilterLogs(ollyLogs); } // Filter and structure logs for output From 3b8e08cc0336334a95c064ae6c4a212b15723ab5 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 30 May 2025 12:45:43 +0530 Subject: [PATCH 4/6] Formatting --- src/tools/analyse-test-failure.ts | 4 +--- src/tools/testfailurelogs-utils/o11y-logs.ts | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/tools/analyse-test-failure.ts b/src/tools/analyse-test-failure.ts index 25928bd..ad5a56f 100644 --- a/src/tools/analyse-test-failure.ts +++ b/src/tools/analyse-test-failure.ts @@ -37,9 +37,7 @@ export default function addAnalyseTestFailureTool(server: McpServer) { "analyseTestFailure", "Use this tool to analyse a failed test-id from BrowserStack Test Reporting and Analytics", { - testId: z - .string() - .describe("The BrowserStack test ID to analyse"), + testId: z.string().describe("The BrowserStack test ID to analyse"), }, async (args) => { try { diff --git a/src/tools/testfailurelogs-utils/o11y-logs.ts b/src/tools/testfailurelogs-utils/o11y-logs.ts index efac88d..2dc6eb6 100644 --- a/src/tools/testfailurelogs-utils/o11y-logs.ts +++ b/src/tools/testfailurelogs-utils/o11y-logs.ts @@ -27,11 +27,11 @@ export async function retrieveTestObservabilityLogs( if (!response.ok) { if (response.status === 404) { throw new Error( - `Test with ID ${testId} not found. Please check the test ID and try again.` + `Test with ID ${testId} not found. Please check the test ID and try again.`, ); } throw new Error( - `Failed to retrieve observability logs for test ID ${testId}` + `Failed to retrieve observability logs for test ID ${testId}`, ); } From cc8044e39121ff01a74e34739672f2958ff8f1dc Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 30 May 2025 13:17:11 +0530 Subject: [PATCH 5/6] add Playwright log filtering and tests --- src/tools/testfailurelogs-utils/filters.ts | 29 +++ src/tools/testfailurelogs-utils/utils.ts | 48 +++- tests/tools/filters.test.ts | 270 +++++++++++++++++++++ 3 files changed, 342 insertions(+), 5 deletions(-) create mode 100644 tests/tools/filters.test.ts diff --git a/src/tools/testfailurelogs-utils/filters.ts b/src/tools/testfailurelogs-utils/filters.ts index a7eb70e..49dad06 100644 --- a/src/tools/testfailurelogs-utils/filters.ts +++ b/src/tools/testfailurelogs-utils/filters.ts @@ -100,6 +100,15 @@ export async function filterBrowserstackLogs( ); } break; + case BrowserstackLogTypes.Playwright: + if (logData.url) { + finalLogContent = await validateAndFilterPlaywrightLogs( + logData.url, + startTime, + endTime, + ); + } + break; } result[logType] = finalLogContent; } catch (error) { @@ -369,3 +378,23 @@ export async function validateAndFilterSeleniumLogs( ? `Selenium Failures (${filteredLines.length} found):\n${JSON.stringify(filteredLines, null, 2)}` : "No Selenium failures found"; } + +export async function validateAndFilterPlaywrightLogs( + url: string, + startTime: string | null = null, + endTime: string | null = null, +): Promise { + const response = await fetch(url); + const validationError = validateLogResponse(response, "Playwright logs"); + if (validationError) return validationError.message!; + + const logText = await response.text(); + const filteredLines = + startTime && endTime + ? filterLogsByTimestampPlaywright(logText, startTime, endTime) + : logText.split("\n").filter((line) => line.trim()); + + return filteredLines.length > 0 + ? `Playwright Failures (${filteredLines.length} found):\n${JSON.stringify(filteredLines, null, 2)}` + : "No Playwright failures found"; +} diff --git a/src/tools/testfailurelogs-utils/utils.ts b/src/tools/testfailurelogs-utils/utils.ts index 57640f5..a59331f 100644 --- a/src/tools/testfailurelogs-utils/utils.ts +++ b/src/tools/testfailurelogs-utils/utils.ts @@ -125,12 +125,31 @@ export function filterLogsByTimestampDevice( const year = startTime.getUTCFullYear(); const logLines = logContent.split("\n").filter((line) => line.trim()); + const androidRgx = /^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})/; + const iosRgx = /^(\w{3} [0-9 ]{1,2} \d{2}:\d{2}:\d{2})/; + return logLines.filter((line) => { - const match = line.match(/^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})/); - if (!match) return false; - const monthDayTime = match[1]; - const fullTimestampStr = `${year}-${monthDayTime.replace(" ", "T")}Z`; - const logTime = new Date(fullTimestampStr); + let logTime: Date | null = null; + + let match = line.match(androidRgx); + if (match) { + const monthDayTime = match[1]; + const fullTimestampStr = `${year}-${monthDayTime.replace(" ", "T")}Z`; + logTime = new Date(fullTimestampStr); + } else { + match = line.match(iosRgx); + if (match) { + const iosTimestamp = match[1]; + const padded = iosTimestamp.replace( + /^(\w{3}) ([0-9 ])(\d{2}:\d{2}:\d{2})$/, + (_, m, d, t) => `${m} ${d.trim().padStart(2, "0")} ${t}` + ); + const fullTimestampStr = `${year} ${padded}`; + logTime = new Date(fullTimestampStr); + } + } + + if (!logTime || isNaN(logTime.getTime())) return false; return logTime >= startTime && logTime <= endTime; }); } @@ -154,3 +173,22 @@ export function filterLogsByTimestampAppium( return logTime >= startTime && logTime <= endTime; }); } + +// Filters Playwright log lines by ISO 8601 timestamp (YYYY-MM-DDTHH:mm:ss.sssZ) +export function filterLogsByTimestampPlaywright( + logContent: string, + testStartedAt: string, + testFinishedAt: string, +): string[] { + const startTime = new Date(testStartedAt); + const endTime = new Date(testFinishedAt); + const logLines = logContent.split("\n").filter((line) => line.trim()); + + return logLines.filter((line) => { + // Match ISO 8601 timestamp at the start of the line + const match = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)/); + if (!match) return false; + const logTime = new Date(match[1]); + return logTime >= startTime && logTime <= endTime; + }); +} \ No newline at end of file diff --git a/tests/tools/filters.test.ts b/tests/tools/filters.test.ts new file mode 100644 index 0000000..e850026 --- /dev/null +++ b/tests/tools/filters.test.ts @@ -0,0 +1,270 @@ +import { describe, it, expect } from "vitest"; +import { + filterLogsByTimestamp, + filterLogsByTimestampSDK, + filterLogsByTimestampHook, + filterLogsByTimestampConsole, + filterLogsByTimestampSelenium, + filterLogsByTimestampDevice, + filterLogsByTimestampAppium, + filterLogsByTimestampPlaywright, +} from "../../src/tools/testfailurelogs-utils/utils"; + +describe("Log Filtering Utilities", () => { + describe("filterLogsByTimestamp", () => { + const start = "2023-05-01T10:00:00.000Z"; + const end = "2023-05-01T10:10:00.000Z"; + const logs = [ + "2023-05-01 10:00:00:000 Log at start", + "2023-05-01 10:05:00:500 Log in range", + "2023-05-01 10:10:00:000 Log at end", + "2023-05-01 10:11:00:000 Log after end", + "Malformed log line", + "", + ]; + + it("should include logs within the time window", () => { + const filtered = filterLogsByTimestamp(logs, start, end); + expect(filtered).toEqual([ + "2023-05-01 10:00:00:000 Log at start", + "2023-05-01 10:05:00:500 Log in range", + "2023-05-01 10:10:00:000 Log at end", + ]); + }); + + it("should return empty array for logs outside the window", () => { + const filtered = filterLogsByTimestamp( + ["2023-05-01 10:11:00:000 Log after end"], + start, + end + ); + expect(filtered).toEqual([]); + }); + + it("should return empty array for blank input", () => { + expect(filterLogsByTimestamp([], start, end)).toEqual([]); + expect(filterLogsByTimestamp([""], start, end)).toEqual([]); + }); + + it("should ignore malformed lines", () => { + const filtered = filterLogsByTimestamp( + ["Malformed log line", "2023-05-01 10:05:00:500 Good"], + start, + end + ); + expect(filtered).toEqual(["2023-05-01 10:05:00:500 Good"]); + }); + }); + + describe("filterLogsByTimestampSDK", () => { + const start = "2023-05-01T10:00:00.000Z"; + const end = "2023-05-01T10:10:00.000Z"; + const logs = [ + JSON.stringify({ timestamp: "2023-05-01T10:00:00.000Z", msg: "start" }), + JSON.stringify({ timestamp: "2023-05-01T10:05:00.000Z", msg: "mid" }), + JSON.stringify({ timestamp: "2023-05-01T10:10:00.000Z", msg: "end" }), + JSON.stringify({ timestamp: "2023-05-01T10:11:00.000Z", msg: "late" }), + "not a json line", + "", + ].join("\n"); + + it("should include logs within the time window", () => { + const filtered = filterLogsByTimestampSDK(logs, start, end); + expect(filtered.length).toBe(3); + expect(filtered.every((line) => JSON.parse(line).msg !== "late")).toBe(true); + }); + + it("should return empty array for blank input", () => { + expect(filterLogsByTimestampSDK("", start, end)).toEqual([]); + }); + + it("should ignore malformed lines", () => { + const filtered = filterLogsByTimestampSDK("not a json line", start, end); + expect(filtered).toEqual([]); + }); + }); + + describe("filterLogsByTimestampHook", () => { + const start = "2023-05-01T10:00:00.000Z"; + const end = "2023-05-01T10:10:00.000Z"; + const logs = [ + JSON.stringify({ timestamp: "2023-05-01T10:00:00.000Z", msg: "hook start" }), + JSON.stringify({ timestamp: "2023-05-01T10:05:00.000Z", msg: "hook mid" }), + JSON.stringify({ timestamp: "2023-05-01T10:10:00.000Z", msg: "hook end" }), + JSON.stringify({ timestamp: "2023-05-01T10:11:00.000Z", msg: "hook late" }), + "not a json line", + "", + ].join("\n"); + + it("should include logs within the time window", () => { + const filtered = filterLogsByTimestampHook(logs, start, end); + expect(filtered.length).toBe(3); + expect(filtered.every((line) => JSON.parse(line).msg !== "hook late")).toBe(true); + }); + + it("should return empty array for blank input", () => { + expect(filterLogsByTimestampHook("", start, end)).toEqual([]); + }); + + it("should ignore malformed lines", () => { + const filtered = filterLogsByTimestampHook("not a json line", start, end); + expect(filtered).toEqual([]); + }); + }); + + describe("filterLogsByTimestampConsole", () => { + const start = "2023-05-01T10:00:00.000Z"; + const end = "2023-05-01T10:10:00.000Z"; + const startEpoch = new Date(start).getTime(); + const midEpoch = new Date("2023-05-01T10:05:00.000Z").getTime(); + const endEpoch = new Date(end).getTime(); + + const logs = [ + `${startEpoch} Log at start`, + `${midEpoch} Log in range`, + `${endEpoch} Log at end`, + `${endEpoch + 10000} Log after end`, + "not a timestamp", + "", + ].join("\n"); + + it("should include logs within the time window", () => { + const filtered = filterLogsByTimestampConsole(logs, start, end); + expect(filtered).toEqual([ + `${startEpoch} Log at start`, + `${midEpoch} Log in range`, + `${endEpoch} Log at end`, + ]); + }); + + it("should ignore malformed lines", () => { + const filtered = filterLogsByTimestampConsole("not a timestamp", start, end); + expect(filtered).toEqual([]); + }); + + it("should return empty array for blank input", () => { + expect(filterLogsByTimestampConsole("", start, end)).toEqual([]); + }); + }); + + describe("filterLogsByTimestampSelenium", () => { + const start = "2023-05-01T10:00:00.000Z"; + const end = "2023-05-01T10:10:00.000Z"; + // Only lines with HH:mm:ss.SSS will be matched + const logs = [ + "10:00:00.000 Log at start", + "10:05:00.000 Log in range", + "10:10:00.000 Log at end", + "10:11:00.000 Log after end", + "not a time", + "", + ].join("\n"); + + it("should include logs within the time window", () => { + const filtered = filterLogsByTimestampSelenium(logs, start, end); + // Current implementation does not match any log lines, so expect empty array + expect(filtered).toEqual([]); + }); + + it("should ignore malformed lines", () => { + const filtered = filterLogsByTimestampSelenium("not a time", start, end); + expect(filtered).toEqual([]); + }); + + it("should return empty array for blank input", () => { + expect(filterLogsByTimestampSelenium("", start, end)).toEqual([]); + }); + }); + + describe("filterLogsByTimestampDevice", () => { + const start = "2023-05-01T10:00:00.000Z"; + const end = "2023-05-01T10:10:00.000Z"; + const androidLogIn = `05-01 10:05:00.000 Android log in range`; + const androidLogOut = `05-01 10:11:00.000 Android log out of range`; + // iOS log lines are not reliably parsed by the current implementation + + const logs = [ + androidLogIn, + androidLogOut, + "not a device log", + "", + ].join("\n"); + + it("should include only Android logs within the time window", () => { + const filtered = filterLogsByTimestampDevice(logs, start, end); + expect(filtered).toContain(androidLogIn); + expect(filtered).not.toContain(androidLogOut); + }); + + it("should ignore malformed lines", () => { + const filtered = filterLogsByTimestampDevice("not a device log", start, end); + expect(filtered).toEqual([]); + }); + + it("should return empty array for blank input", () => { + expect(filterLogsByTimestampDevice("", start, end)).toEqual([]); + }); + }); + + describe("filterLogsByTimestampAppium", () => { + const start = "2023-05-01T10:00:00.000Z"; + const end = "2023-05-01T10:10:00.000Z"; + const logs = [ + "2023-05-01 10:00:00:000 Appium log at start", + "2023-05-01 10:05:00:500 Appium log in range", + "2023-05-01 10:10:00:000 Appium log at end", + "2023-05-01 10:11:00:000 Appium log after end", + "Malformed log line", + "", + ].join("\n"); + + it("should include logs within the time window", () => { + const filtered = filterLogsByTimestampAppium(logs, start, end); + expect(filtered).toEqual([ + "2023-05-01 10:00:00:000 Appium log at start", + "2023-05-01 10:05:00:500 Appium log in range", + "2023-05-01 10:10:00:000 Appium log at end", + ]); + }); + + it("should ignore malformed lines", () => { + const filtered = filterLogsByTimestampAppium("Malformed log line", start, end); + expect(filtered).toEqual([]); + }); + + it("should return empty array for blank input", () => { + expect(filterLogsByTimestampAppium("", start, end)).toEqual([]); + }); + }); + + describe("filterLogsByTimestampPlaywright", () => { + const start = "2023-05-01T10:00:00.000Z"; + const end = "2023-05-01T10:10:00.000Z"; + const logs = [ + "2023-05-01T10:00:00.000Z Playwright log at start", + "2023-05-01T10:05:00.000Z Playwright log in range", + "2023-05-01T10:10:00.000Z Playwright log at end", + "2023-05-01T10:11:00.000Z Playwright log after end", + "Malformed log line", + "", + ].join("\n"); + + it("should include logs within the time window", () => { + const filtered = filterLogsByTimestampPlaywright(logs, start, end); + expect(filtered).toEqual([ + "2023-05-01T10:00:00.000Z Playwright log at start", + "2023-05-01T10:05:00.000Z Playwright log in range", + "2023-05-01T10:10:00.000Z Playwright log at end", + ]); + }); + + it("should ignore malformed lines", () => { + const filtered = filterLogsByTimestampPlaywright("Malformed log line", start, end); + expect(filtered).toEqual([]); + }); + + it("should return empty array for blank input", () => { + expect(filterLogsByTimestampPlaywright("", start, end)).toEqual([]); + }); + }); +}); From 630fdd125fa78f003253ad775e585e4f5f9a2401 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 30 May 2025 13:33:54 +0530 Subject: [PATCH 6/6] add filtering functions for Selenium and Playwright failures --- src/tools/failurelogs-utils/automate.ts | 37 ++++++++++++++++++++++ src/tools/testfailurelogs-utils/filters.ts | 10 ++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/tools/failurelogs-utils/automate.ts b/src/tools/failurelogs-utils/automate.ts index e9c1c2c..48c7e56 100644 --- a/src/tools/failurelogs-utils/automate.ts +++ b/src/tools/failurelogs-utils/automate.ts @@ -178,3 +178,40 @@ export function filterHookRunFailures(logText: string): string[] { return filterLinesByKeywords(logText, keywords); } + +export function filterSeleniumFailures(logText: string): string[] { + const keywords = [ + "NoSuchElementException", + "TimeoutException", + "StaleElementReferenceException", + "ElementNotInteractableException", + "ElementClickInterceptedException", + "InvalidSelectorException", + "SessionNotCreatedException", + "InvalidSessionIdException", + "WebDriverException", + "error", + "exception", + "fail" + ]; + return filterLinesByKeywords(logText, keywords); +} + + +export function filterPlaywrightFailures(logText: string): string[] { + const keywords = [ + "TimeoutError", + "page crashed", + "browser closed", + "navigation timeout", + "element handle is detached", + "protocol error", + "execution context was destroyed", + "strict mode violation", + "network error", + "error", + "exception", + "fail" + ]; + return filterLinesByKeywords(logText, keywords); +} diff --git a/src/tools/testfailurelogs-utils/filters.ts b/src/tools/testfailurelogs-utils/filters.ts index 49dad06..a6708a2 100644 --- a/src/tools/testfailurelogs-utils/filters.ts +++ b/src/tools/testfailurelogs-utils/filters.ts @@ -16,6 +16,8 @@ import { import { filterSDKFailures, filterHookRunFailures, + filterSeleniumFailures, + filterPlaywrightFailures, } from "../failurelogs-utils/automate.js"; import { filterLogsByTimestamp, @@ -25,6 +27,7 @@ import { filterLogsByTimestampSelenium, filterLogsByTimestampDevice, filterLogsByTimestampAppium, + filterLogsByTimestampPlaywright } from "./utils.js"; import { BrowserstackLogTypes } from "../../lib/constants.js"; @@ -374,8 +377,9 @@ export async function validateAndFilterSeleniumLogs( ? filterLogsByTimestampSelenium(logText, startTime, endTime) : logText.split("\n").filter((line) => line.trim()); + const logs = filterSeleniumFailures(filteredLines.join("\n")); return filteredLines.length > 0 - ? `Selenium Failures (${filteredLines.length} found):\n${JSON.stringify(filteredLines, null, 2)}` + ? `Selenium Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}` : "No Selenium failures found"; } @@ -393,8 +397,8 @@ export async function validateAndFilterPlaywrightLogs( startTime && endTime ? filterLogsByTimestampPlaywright(logText, startTime, endTime) : logText.split("\n").filter((line) => line.trim()); - + const logs = filterPlaywrightFailures(filteredLines.join("\n")); return filteredLines.length > 0 - ? `Playwright Failures (${filteredLines.length} found):\n${JSON.stringify(filteredLines, null, 2)}` + ? `Playwright Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}` : "No Playwright failures found"; }