Skip to content

Commit 40352e9

Browse files
tsemachhjunie-agent
andcommitted
feat(replay): per-row match indicator + move status above tabs
Co-authored-by: Junie <junie@jetbrains.com>
1 parent 9a23b1a commit 40352e9

6 files changed

Lines changed: 47 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on Keep a Changelog,
66
and this project adheres to Semantic Versioning.
77

8+
## [1.0.14] - 2026-05-03
9+
### Changed
10+
- Replay status indicator moved above the Record/Replay tab buttons so the current state is visible regardless of the active tab.
11+
- Per-request match indicator: each row in the API requests list now shows a green ● when it has been matched at least once during the current replay (gray when not yet matched, faded when not replaying). Matched rows are highlighted with a subtle green tint. Replaces the previous reliance on the global Matched/Unmatched stats which under-counted because of de-duplication by path.
12+
813
## [1.0.13] - 2026-05-03
914
### Changed
1015
- Replay stats now only count requests within the recording's URL filter (e.g. `/api`). Out-of-scope analytics calls (e.g. `/ingest/*`) no longer pollute Matched/Unmatched counters.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apireplay",
3-
"version": "1.0.13",
3+
"version": "1.0.14",
44
"private": true,
55
"type": "module",
66
"scripts": {

src/background/replayer.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export async function startReplaying(
9797
}
9898

9999
await chrome.storage.local.set({ replayedRequests: {} });
100-
await chrome.storage.session.set({ replayStats: { matched: 0, unmatched: 0, unmatchedUrls: [], hitCount: {} } });
100+
await chrome.storage.session.set({ replayStats: { matched: 0, unmatched: 0, unmatchedUrls: [], hitCount: {}, matchedKeys: {} } });
101101
await chrome.debugger.attach({ tabId: tabs[0].id }, '1.0');
102102
await chrome.debugger.sendCommand({ tabId: tabs[0].id }, 'Network.enable');
103103
await chrome.debugger.sendCommand({ tabId: tabs[0].id }, 'Fetch.enable', {
@@ -176,7 +176,7 @@ export async function onReplayerEvent(store: StateStore, tabId: number, message:
176176
}
177177

178178
const requests = state.recordedData?.requests || state.recordedData || {};
179-
const requestValues = Object.values(requests) as Array<any>;
179+
const requestEntries = Object.entries(requests) as Array<[string, any]>;
180180

181181
const requestUrl = getEventRequestUrl(params);
182182
if (!requestUrl) {
@@ -199,18 +199,21 @@ export async function onReplayerEvent(store: StateStore, tabId: number, message:
199199
return;
200200
}
201201

202-
let matched = requestValues.find(
203-
(item) => methodsMatch(item.method, incomingMethod) && getPathname(item.url) === incomingPathname
202+
let matchedEntry = requestEntries.find(
203+
([, item]) => methodsMatch(item.method, incomingMethod) && getPathname(item.url) === incomingPathname
204204
);
205205

206-
if (!matched && state.fallbackMatchingEnabled) {
207-
matched = requestValues.find((item) => {
206+
if (!matchedEntry && state.fallbackMatchingEnabled) {
207+
matchedEntry = requestEntries.find(([, item]) => {
208208
if (!methodsMatch(item.method, incomingMethod)) return false;
209209
const candidatePath = getPathname(item.url);
210210
return candidatePath.split('/').filter(Boolean).length === incomingPathname.split('/').filter(Boolean).length;
211211
});
212212
}
213213

214+
const matched = matchedEntry?.[1];
215+
const matchedKey = matchedEntry?.[0];
216+
214217
if (!matched) {
215218
const replayStatsData = await chrome.storage.session.get('replayStats');
216219
const replayStats = replayStatsData.replayStats || { matched: 0, unmatched: 0, unmatchedUrls: [], hitCount: {} };
@@ -233,10 +236,14 @@ export async function onReplayerEvent(store: StateStore, tabId: number, message:
233236
await chrome.storage.local.set({ replayedRequests });
234237

235238
const replayStatsData = await chrome.storage.session.get(['replayStats', 'replayOptions']);
236-
const replayStats = replayStatsData.replayStats || { matched: 0, unmatched: 0, unmatchedUrls: [], hitCount: {} };
239+
const replayStats = replayStatsData.replayStats || { matched: 0, unmatched: 0, unmatchedUrls: [], hitCount: {}, matchedKeys: {} };
237240
replayStats.matched += 1;
238241
replayStats.hitCount = replayStats.hitCount || {};
239242
replayStats.hitCount[path] = (replayStats.hitCount[path] || 0) + 1;
243+
if (matchedKey) {
244+
replayStats.matchedKeys = replayStats.matchedKeys || {};
245+
replayStats.matchedKeys[matchedKey] = (replayStats.matchedKeys[matchedKey] || 0) + 1;
246+
}
240247
await chrome.storage.session.set({ replayStats });
241248

242249
const latency = resolveLatency(replayStatsData.replayOptions);

src/popup.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ <h2 class="text-xl font-bold">API Replay</h2>
1818
</button>
1919
</div>
2020

21+
<div id="statusIndicator" class="mb-2 p-1 bg-gray-200 dark:bg-gray-700 rounded text-center text-sm font-semibold"></div>
22+
2123
<div class="mb-3 flex gap-2">
2224
<button id="recordTabButton" class="tab-button tab-button-active px-3 py-1 rounded text-sm font-semibold" type="button">Record</button>
2325
<button id="replayTabButton" class="tab-button px-3 py-1 rounded text-sm font-semibold" type="button">Replay &amp; Preview</button>
2426
</div>
2527

26-
<div id="statusIndicator" class="mb-2 p-1 bg-gray-200 dark:bg-gray-700 rounded text-center text-sm font-semibold"></div>
27-
2828
<div id="recordTabPanel" class="tab-panel">
2929
<div class="mb-4">
3030
<div class="grid grid-cols-1 gap-2 mt-2">

src/popup/components/ApiPreview.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ export function renderApiPaths(
66
requestHitCounts: Record<string, number>,
77
onSelectPath: (path: string) => void,
88
onToggleEnabled: (requestKey: string, enabled: boolean) => void,
9-
onUpdateStatus: (requestKey: string, status: number | undefined) => void
9+
onUpdateStatus: (requestKey: string, status: number | undefined) => void,
10+
options: { matchedKeys?: Record<string, number>; isReplaying?: boolean } = {}
1011
) {
12+
const matchedKeys = options.matchedKeys || {};
13+
const isReplaying = options.isReplaying === true;
1114
const rows = Object.entries(requests).sort(([a], [b]) => a.localeCompare(b));
1215

1316
container.innerHTML = `
@@ -17,7 +20,8 @@ export function renderApiPaths(
1720
<table class="w-full text-xs">
1821
<thead class="bg-gray-200 dark:bg-gray-800">
1922
<tr>
20-
<th class="text-left p-1">Replay</th>
23+
<th class="text-left p-1" title="Include this request when replaying">Replay</th>
24+
<th class="text-left p-1" title="${isReplaying ? 'Green = matched at least once during replay; gray = not yet matched' : 'Match indicator is shown during replay'}">●</th>
2125
<th class="text-left p-1">Method</th>
2226
<th class="text-left p-1">Path</th>
2327
<th class="text-left p-1">Status</th>
@@ -33,11 +37,23 @@ export function renderApiPaths(
3337
const replayCount = requestHitCounts[pathWithoutQuery] || 0;
3438
const statusValue = typeof request.status === 'number' ? String(request.status) : '';
3539
const enabled = request.enabled !== false;
40+
const matchHits = matchedKeys[key] || 0;
41+
const matchColor = !isReplaying
42+
? 'text-gray-300 dark:text-gray-600'
43+
: matchHits > 0
44+
? 'text-green-500'
45+
: 'text-gray-400 dark:text-gray-500';
46+
const matchTitle = !isReplaying
47+
? 'Match indicator is shown during replay'
48+
: matchHits > 0
49+
? `Matched ${matchHits} time(s) during current replay`
50+
: 'Not matched yet during current replay';
3651
return `
37-
<tr class="border-t border-gray-200 dark:border-gray-700">
52+
<tr class="border-t border-gray-200 dark:border-gray-700${isReplaying && matchHits > 0 ? ' bg-green-50 dark:bg-green-900/20' : ''}">
3853
<td class="p-1">
3954
<input type="checkbox" class="request-enabled-toggle" data-request-key="${encodeURIComponent(key)}" ${enabled ? 'checked' : ''}>
4055
</td>
56+
<td class="p-1 text-center ${matchColor}" title="${matchTitle}">●</td>
4157
<td class="p-1">${request.method}</td>
4258
<td class="p-1 cursor-pointer hover:text-blue-500" data-path="${path}">${path}</td>
4359
<td class="p-1">

src/popup/main.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ document.addEventListener('DOMContentLoaded', () => {
472472
return (requestSearchInput?.value || '').trim().toLowerCase();
473473
}
474474

475-
function renderPreviewList(container, recordingName, requests, requestHitCounts) {
475+
function renderPreviewList(container, recordingName, requests, requestHitCounts, matchedKeys = {}) {
476476
if (!container) {
477477
return;
478478
}
@@ -499,7 +499,8 @@ document.addEventListener('DOMContentLoaded', () => {
499499
}
500500
updateApiPreview();
501501
});
502-
}
502+
},
503+
{ matchedKeys, isReplaying }
503504
);
504505
}
505506

@@ -555,9 +556,10 @@ document.addEventListener('DOMContentLoaded', () => {
555556
...replayedRequests,
556557
...replayStatsHitCount
557558
};
559+
const matchedKeys = replaySessionData.replayStats?.matchedKeys || {};
558560

559-
renderPreviewList(apiPreviewDiv, name, filteredRequests, requestHitCounts);
560-
renderPreviewList(recordApiPreviewDiv, name, allRequests, requestHitCounts);
561+
renderPreviewList(apiPreviewDiv, name, filteredRequests, requestHitCounts, matchedKeys);
562+
renderPreviewList(recordApiPreviewDiv, name, allRequests, requestHitCounts, matchedKeys);
561563
})
562564
.catch((error) => {
563565
console.error('Load recording error:', error);

0 commit comments

Comments
 (0)