Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 88 additions & 2 deletions src/channelHandlers/browserstack-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import CONFIG from "../constants/config"
import { parseAutomateSessionLogs } from "../utils/latency-finder/session-logs-parser"
import { parseAutomateSeleniumLogs } from "../utils/latency-finder/selenium-logs-parser"
import { convertUTCToEpoch } from "../utils/latency-finder/helper"
import { dialog } from 'electron';
import fs from 'fs'
import path from "path";

Expand Down Expand Up @@ -316,4 +315,91 @@ export const uploadApp = async (filePath: string) => {
console.error("Upload failed:", err.message);
throw err;
}
};
};

export const getAppAutomateNetworkLogs = async (session: AutomateSessionResponse) => {
// Extract build_id and session_id from the session
const buildId = session.automation_session.browser_url?.match(/\/builds\/([^\/]+)\//)?.[1];
const sessionId = session.automation_session.hashed_id;

if (!buildId || !sessionId) {
return 'No network logs available for this session';
}

try {
const networkLogsUrl = `${BASE_URL}/app-automate/builds/${buildId}/sessions/${sessionId}/networklogs`;
const response = await fetch(networkLogsUrl, {
headers: {
"Authorization": getAuth()
}
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.text();
} catch (error) {
console.error('Failed to fetch App Automate network logs:', error);
return 'Failed to load network logs';
}
}

export const getAppAutomateSessionCapabilities = async (session: AutomateSessionResponse) => {
const logs = await download(session.automation_session.logs);
const lines = logs.split('\n');

const timestampRegex = /^\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}:\d{1,3}/;

const entries: string[] = [];

for (const line of lines) {
if (timestampRegex.test(line)) {
// New log entry → push as a new entry
entries.push(line);
} else if (entries.length > 0) {
// Continuation of previous entry → append
entries[entries.length - 1] += '\n' + line;
} else {
// Edge case: first line doesn't start with timestamp
entries.push(line);
}
}

// Parse capabilities from the logs - this is a simplified version
const capabilities: any[] = [];

// Look for capability information in the logs
for (const entry of entries) {
if (entry.includes('capabilities') || entry.includes('Capabilities')) {
try {
// Try to extract JSON from the entry
const jsonMatch = entry.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const caps = JSON.parse(jsonMatch[0]);
capabilities.push(caps);
}
} catch (e) {
// If parsing fails, continue
}
}
}

// If no capabilities found in logs, create a basic structure from session info
if (capabilities.length === 0) {
const basicCaps = {
platformName: session.automation_session.os,
platformVersion: session.automation_session.os_version,
deviceName: session.automation_session.device,
app: session.automation_session.app_details?.app_url,
'appium:deviceName': session.automation_session.device,
'appium:platformName': session.automation_session.os,
'appium:platformVersion': session.automation_session.os_version,
'appium:app': session.automation_session.app_details?.app_url,
};
capabilities.push(basicCaps);
}

return {
capabilities
};
}
4 changes: 3 additions & 1 deletion src/constants/ipc-channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ const CHANNELS = {
GET_BROWSERSTACK_APP_AUTOMATE_PARSED_TEXT_LOGS: 'GET_BROWSERSTACK_APP_AUTOMATE_PARSED_TEXT_LOGS',
GET_UPLOADED_APPS: 'GET_UPLOADED_APPS',
UPLOAD_APP: 'UPLOAD_APP',
ELECTRON_OPEN_APP_PICKER: 'ELECTRON_OPEN_APP_PICKER'
ELECTRON_OPEN_APP_PICKER: 'ELECTRON_OPEN_APP_PICKER',
GET_BROWSERSTACK_APP_AUTOMATE_CAPABILITIES: 'GET_BROWSERSTACK_APP_AUTOMATE_CAPABILITIES',
GET_BROWSERSTACK_APP_AUTOMATE_NETWORK_LOGS: 'GET_BROWSERSTACK_APP_AUTOMATE_NETWORK_LOGS',
}

export default CHANNELS
2 changes: 2 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ declare global {
getAppAutomateParsedTextLogs: (session: AppAutomateSessionResponse) => Promise<ParsedTextLogsResult>
getUploadedApps: () => Promise<UploadedApp[]>
uploadApp: (filePath: string) => Promise<void>
getAppAutomateNetworkLogs: (session: AppAutomateSessionResponse) => Promise<string>
getAppAutomateSessionCapabilities: (session: AppAutomateSessionResponse) => Promise<{ capabilities: any[] }>
}

type ElectronAPI = {
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import StorageKeys from './constants/storage-keys';
import CONFIG from './constants/config';

import { mkdirSync } from 'fs'
import { executeCommand, getAutomateSessionDetails, getParsedAutomateTextLogs, startBrowserStackSession, stopBrowserStackSession, getAutomateParsedSeleniumLogs, getAutomateParsedSessionLogs, getSeleniumLogs, getHarLogs, getAppAutomateSessionDetails, getAppAutomateParsedTextLogs, getAllUploadedApps, uploadApp } from './channelHandlers/browserstack-api';
import { executeCommand, getAutomateSessionDetails, getParsedAutomateTextLogs, startBrowserStackSession, stopBrowserStackSession, getAutomateParsedSeleniumLogs, getAutomateParsedSessionLogs, getSeleniumLogs, getHarLogs, getAppAutomateSessionDetails, getAppAutomateParsedTextLogs, getAllUploadedApps, uploadApp, getAppAutomateNetworkLogs, getAppAutomateSessionCapabilities } from './channelHandlers/browserstack-api';
import { openExternalUrl, openAppPicker } from './channelHandlers/electron-api';


Expand Down Expand Up @@ -107,6 +107,8 @@ app.whenReady().then(() => {
ipcMain.handle(CHANNELS.GET_UPLOADED_APPS, () => getAllUploadedApps())
ipcMain.handle(CHANNELS.UPLOAD_APP, (_, filePath) => uploadApp(filePath))
ipcMain.handle(CHANNELS.ELECTRON_OPEN_APP_PICKER, () => openAppPicker())
ipcMain.handle(CHANNELS.GET_BROWSERSTACK_APP_AUTOMATE_NETWORK_LOGS, (_, session) => getAppAutomateNetworkLogs(session))
ipcMain.handle(CHANNELS.GET_BROWSERSTACK_APP_AUTOMATE_CAPABILITIES, (_, session) => getAppAutomateSessionCapabilities(session))
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
2 changes: 2 additions & 0 deletions src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const browserstackAPI: BrowserStackAPI = {
getAppAutomateParsedTextLogs: (session) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_APP_AUTOMATE_PARSED_TEXT_LOGS, session),
getUploadedApps: () => ipcRenderer.invoke(CHANNELS.GET_UPLOADED_APPS),
uploadApp: (filePath: string) => ipcRenderer.invoke(CHANNELS.UPLOAD_APP, filePath),
getAppAutomateNetworkLogs: (session) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_APP_AUTOMATE_NETWORK_LOGS, session),
getAppAutomateSessionCapabilities: (session) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_APP_AUTOMATE_CAPABILITIES, session),
}

const electronAPI: ElectronAPI = {
Expand Down
7 changes: 4 additions & 3 deletions src/renderer/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import LatencyFinder from "./routes/automate/tools/latency-finder";
import SessionComparison from "./routes/automate/tools/session-comparison";
import AppReplayTool from "./routes/app-automate/tools/replay-tool";
import WebReplayTool from "./routes/automate/tools/replay-tool";
import AppAutomateSessionComparison from "./routes/app-automate/tools/session-comparison";

const Products: {
name: string
Expand Down Expand Up @@ -50,14 +51,14 @@ const Products: {
title: "Latency Analyser",
description:
"Analyses time spend on different actions. Helpful to identify inside/outside time for a customer session.",
path: "/automate/latency-analyser",
path: "/app-automate/latency-analyser",
component: null,
},
{
title: "Session Comparison",
description: "Compares logs across sessions and highlights differences",
path: '/automate/session-comparison',
component: null
path: '/app-automate/session-comparison',
component: AppAutomateSessionComparison
}
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { useMemo, useState, useEffect } from 'react';
import { DiffLine } from '../types';
import { generateDiff } from '../utils/diffAlgorithm';

interface DiffViewerProps {
oldValue: string;
newValue: string;
leftTitle: string;
rightTitle: string;
batchSize?: number;
}

function LineContent({ line, side }: { line: DiffLine; side: 'left' | 'right' }) {
const content = side === 'left' ? line.leftLine : line.rightLine;

if (content === null) return <span className="text-gray-400">—</span>;
if (content === '') return <span className="text-gray-300">∅</span>;

if (line.type === 'modified' && line.charDiffs) {
return (
<span>
{line.charDiffs.map((diff, idx) => {
if (diff.type === 'common') {
return <span key={idx}>{diff.text}</span>;
} else if (diff.type === 'removed' && side === 'left') {
return (
<span key={idx} className="bg-red-200 text-red-900 font-semibold">
{diff.text}
</span>
);
} else if (diff.type === 'added' && side === 'right') {
return (
<span key={idx} className="bg-green-200 text-green-900 font-semibold">
{diff.text}
</span>
);
}
return null;
})}
</span>
);
}

return <span>{content}</span>;
}

export default function DiffViewer({
oldValue,
newValue,
leftTitle,
rightTitle,
batchSize = 200
}: DiffViewerProps) {
const allDiffLines = useMemo(() => {
return generateDiff(oldValue, newValue);
}, [oldValue, newValue]);

const [renderLimit, setRenderLimit] = useState(batchSize);

useEffect(() => {
setRenderLimit(batchSize);
}, [oldValue, newValue, batchSize]);

const visibleLines = allDiffLines.slice(0, renderLimit);
const remainingLines = allDiffLines.length - renderLimit;

const handleLoadMore = () => {
setRenderLimit((prev) => Math.min(prev + batchSize, allDiffLines.length));
};

const handleLoadAll = () => {
setRenderLimit(allDiffLines.length);
};

const getLineStyle = (type: DiffLine['type'], side: 'left' | 'right') => {
if (type === 'unchanged') return 'bg-white';
if (type === 'removed' && side === 'left') return 'bg-red-50';
if (type === 'added' && side === 'right') return 'bg-green-50';
if (type === 'modified') return side === 'left' ? 'bg-orange-50' : 'bg-blue-50';
if (type === 'removed' && side === 'right') return 'bg-gray-50';
if (type === 'added' && side === 'left') return 'bg-gray-50';
return 'bg-white';
};

const getIndicator = (type: DiffLine['type'], side: 'left' | 'right') => {
if (type === 'unchanged') return '•';
if (type === 'removed' && side === 'left') return '−';
if (type === 'added' && side === 'right') return '+';
if (type === 'modified') return '~';
return '•';
};

return (
<div className="flex flex-col h-full border border-gray-300 rounded-lg overflow-hidden bg-white">
<div className="grid grid-cols-2 border-b border-gray-300 bg-gray-50 sticky top-0 z-10">
<div className="px-4 py-2 font-semibold border-r border-gray-300">
{leftTitle}
</div>
<div className="px-4 py-2 font-semibold">
{rightTitle}
</div>
</div>

<div className="overflow-auto flex-1">
{visibleLines.map((line, idx) => (
<div key={idx} className="grid grid-cols-2 border-b border-gray-200 hover:bg-gray-50">
<div className={`flex ${getLineStyle(line.type, 'left')} border-r border-gray-200`}>
<div className="w-12 flex-shrink-0 text-right pr-2 py-1 text-xs text-gray-500 bg-gray-50 border-r border-gray-200 select-none">
{line.leftNumber || ''}
</div>
<div className="w-8 flex-shrink-0 text-center py-1 text-xs font-bold select-none">
{getIndicator(line.type, 'left')}
</div>
<div className="flex-1 px-2 py-1 font-mono text-sm whitespace-pre-wrap break-all">
<LineContent line={line} side="left" />
</div>
</div>

<div className={`flex ${getLineStyle(line.type, 'right')}`}>
<div className="w-12 flex-shrink-0 text-right pr-2 py-1 text-xs text-gray-500 bg-gray-50 border-r border-gray-200 select-none">
{line.rightNumber || ''}
</div>
<div className="w-8 flex-shrink-0 text-center py-1 text-xs font-bold select-none">
{getIndicator(line.type, 'right')}
</div>
<div className="flex-1 px-2 py-1 font-mono text-sm whitespace-pre-wrap break-all">
<LineContent line={line} side="right" />
</div>
</div>
</div>
))}

{remainingLines > 0 && (
<div className="p-4 bg-gray-50 border-t border-gray-300">
<div className="text-sm text-gray-600 mb-2">
Showing {renderLimit.toLocaleString()} of {allDiffLines.length.toLocaleString()} lines.
</div>
<div className="flex gap-2">
<button
onClick={handleLoadMore}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
>
Load next {batchSize} lines
</button>
{remainingLines < 5000 && (
<button
onClick={handleLoadAll}
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition"
>
Load Remaining ({remainingLines})
</button>
)}
</div>
</div>
)}
</div>

<div className="grid grid-cols-2 border-t border-gray-300 bg-gray-50 text-xs">
<div className="px-4 py-2 border-r border-gray-300 flex gap-4">
<span className="flex items-center gap-1">
<span className="font-bold">−</span> Removed
</span>
<span className="flex items-center gap-1">
<span className="font-bold">~</span> Modified
</span>
</div>
<div className="px-4 py-2 flex gap-4">
<span className="flex items-center gap-1">
<span className="font-bold">+</span> Added
</span>
<span className="flex items-center gap-1">
<span className="font-bold">~</span> Modified
</span>
</div>
</div>
</div>
);
}
Loading