mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat(traffic-inspector): live (in-flight) request filter (Gap 5) (#4130)
Integrated into release/v3.8.29
This commit is contained in:
committed by
GitHub
parent
91183dc27e
commit
1faf72bbda
@@ -19,8 +19,16 @@ export function TrafficInspectorPageClient() {
|
||||
const [containerHeight, setContainerHeight] = useState(600);
|
||||
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [selectedRequest, setSelectedRequest] = useState<InterceptedRequest | null>(null);
|
||||
const { filters, setProfile, setHost, setAgent, setStatus, setSessionId, setSameContext } =
|
||||
useTrafficFilters();
|
||||
const {
|
||||
filters,
|
||||
setProfile,
|
||||
setHost,
|
||||
setAgent,
|
||||
setStatus,
|
||||
setSessionId,
|
||||
setSameContext,
|
||||
toggleLive,
|
||||
} = useTrafficFilters();
|
||||
const [{ listWidth, collapsed }, { startDrag, toggleCollapse }] = useResizablePanels();
|
||||
const [streamState, streamActions] = useTrafficStream(filters);
|
||||
const recorder = useSessionRecorder();
|
||||
@@ -108,6 +116,8 @@ export function TrafficInspectorPageClient() {
|
||||
onHostChange={setHost}
|
||||
onAgentChange={setAgent}
|
||||
onStatusChange={setStatus}
|
||||
liveOnly={filters.liveOnly ?? false}
|
||||
onToggleLive={toggleLive}
|
||||
paused={streamState.paused}
|
||||
onPause={streamActions.pause}
|
||||
onResume={streamActions.resume}
|
||||
|
||||
@@ -19,6 +19,8 @@ interface TopBarControlsProps {
|
||||
onHostChange: (h: string | undefined) => void;
|
||||
onAgentChange: (a: AgentId | undefined) => void;
|
||||
onStatusChange: (s: ListFilters["status"]) => void;
|
||||
liveOnly: boolean;
|
||||
onToggleLive: () => void;
|
||||
paused: boolean;
|
||||
onPause: () => void;
|
||||
onResume: () => void;
|
||||
@@ -46,6 +48,8 @@ export function TopBarControls({
|
||||
onHostChange,
|
||||
onAgentChange,
|
||||
onStatusChange,
|
||||
liveOnly,
|
||||
onToggleLive,
|
||||
paused,
|
||||
onPause,
|
||||
onResume,
|
||||
@@ -125,6 +129,25 @@ export function TopBarControls({
|
||||
<option value="error">error</option>
|
||||
</select>
|
||||
|
||||
{/* Live (in-flight) toggle — Gap 5 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleLive}
|
||||
aria-pressed={liveOnly}
|
||||
title={t("liveOnly")}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded border px-2 py-1 text-xs focus-ring",
|
||||
liveOnly
|
||||
? "border-green-500/50 bg-green-500/15 text-green-500"
|
||||
: "border-border text-text-muted hover:text-text-main"
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]" aria-hidden="true">
|
||||
sensors
|
||||
</span>
|
||||
{t("liveOnly")}
|
||||
</button>
|
||||
|
||||
{/* Action buttons */}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -5,6 +5,8 @@ import type { ListFilters } from "@/mitm/inspector/types";
|
||||
|
||||
export interface FiltersState extends ListFilters {
|
||||
sameContextKey?: string;
|
||||
/** Show only in-flight (open) requests — Gap 5 "Live" filter. */
|
||||
liveOnly?: boolean;
|
||||
}
|
||||
|
||||
export function useTrafficFilters() {
|
||||
@@ -38,6 +40,10 @@ export function useTrafficFilters() {
|
||||
setFilters((prev) => ({ ...prev, sameContextKey: contextKey }));
|
||||
}, []);
|
||||
|
||||
const toggleLive = useCallback(() => {
|
||||
setFilters((prev) => ({ ...prev, liveOnly: !prev.liveOnly }));
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setFilters({ profile: "llm" });
|
||||
}, []);
|
||||
@@ -51,6 +57,7 @@ export function useTrafficFilters() {
|
||||
setSource,
|
||||
setSessionId,
|
||||
setSameContext,
|
||||
toggleLive,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { InterceptedRequest, ListFilters, WsEvent } from "@/mitm/inspector/types";
|
||||
import { matchesTrafficFilter } from "@/lib/inspector/matchesTrafficFilter";
|
||||
import type { FiltersState } from "./useTrafficFilters";
|
||||
|
||||
const WS_PATH = "/api/tools/traffic-inspector/ws";
|
||||
@@ -47,24 +48,7 @@ export function useTrafficStream(
|
||||
});
|
||||
|
||||
const applyFilter = useCallback((req: InterceptedRequest): boolean => {
|
||||
const f = filtersRef.current as FiltersState;
|
||||
if (f.profile === "llm" && req.detectedKind !== "llm") return false;
|
||||
if (f.profile === "custom" && req.source !== "custom-host") return false;
|
||||
if (f.host && !req.host.includes(f.host)) return false;
|
||||
if (f.agent && req.agent !== f.agent) return false;
|
||||
if (f.source && req.source !== f.source) return false;
|
||||
if (f.sessionId && req.sessionId !== f.sessionId) return false;
|
||||
if (f.sameContextKey && req.contextKey !== f.sameContextKey) return false;
|
||||
if (f.status) {
|
||||
const s = req.status;
|
||||
if (typeof s === "number") {
|
||||
const cat = `${Math.floor(s / 100)}xx`;
|
||||
if (cat !== f.status) return false;
|
||||
} else if (f.status === "error" && s !== "error") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return matchesTrafficFilter(req, filtersRef.current as FiltersState);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -8578,6 +8578,7 @@
|
||||
"httpProxyTitle": "HTTP Proxy Snippet — port {port}",
|
||||
"notRecording": "Not recording",
|
||||
"anyStatus": "Any status",
|
||||
"liveOnly": "Live",
|
||||
"viewingRecordedSession": "Viewing recorded session",
|
||||
"backToLive": "Back to live",
|
||||
"untitledSession": "Untitled session",
|
||||
|
||||
37
src/lib/inspector/matchesTrafficFilter.ts
Normal file
37
src/lib/inspector/matchesTrafficFilter.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Pure request-list filter for the Traffic Inspector.
|
||||
*
|
||||
* Extracted from the inline `applyFilter` closure inside the useTrafficStream
|
||||
* hook so the filtering rules are unit-testable. Behaviour is identical to the
|
||||
* previous inline logic, plus the new `liveOnly` toggle (Gap 5) that keeps only
|
||||
* in-flight requests — letting the user watch open connections in real time.
|
||||
*/
|
||||
import type { InterceptedRequest, ListFilters } from "@/mitm/inspector/types";
|
||||
|
||||
export type TrafficFilters = ListFilters & {
|
||||
/** Client-only: restrict the list to the same system-prompt context. */
|
||||
sameContextKey?: string;
|
||||
/** Client-only: show only in-flight (open) requests (Gap 5). */
|
||||
liveOnly?: boolean;
|
||||
};
|
||||
|
||||
export function matchesTrafficFilter(req: InterceptedRequest, f: TrafficFilters): boolean {
|
||||
if (f.profile === "llm" && req.detectedKind !== "llm") return false;
|
||||
if (f.profile === "custom" && req.source !== "custom-host") return false;
|
||||
if (f.host && !req.host.includes(f.host)) return false;
|
||||
if (f.agent && req.agent !== f.agent) return false;
|
||||
if (f.source && req.source !== f.source) return false;
|
||||
if (f.sessionId && req.sessionId !== f.sessionId) return false;
|
||||
if (f.sameContextKey && req.contextKey !== f.sameContextKey) return false;
|
||||
if (f.liveOnly && req.status !== "in-flight") return false;
|
||||
if (f.status) {
|
||||
const s = req.status;
|
||||
if (typeof s === "number") {
|
||||
const cat = `${Math.floor(s / 100)}xx`;
|
||||
if (cat !== f.status) return false;
|
||||
} else if (f.status === "error" && s !== "error") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
89
tests/unit/traffic-filter-live.test.ts
Normal file
89
tests/unit/traffic-filter-live.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Gap 5 — "Live" (in-flight) filter for the Traffic Inspector.
|
||||
*
|
||||
* The request-list filter logic used to live inline inside the useTrafficStream
|
||||
* hook (untested). It is extracted here into a pure `matchesTrafficFilter` so the
|
||||
* new `liveOnly` toggle — and, for the first time, the pre-existing profile/host/
|
||||
* agent/status rules — are unit-tested. These tests pin BOTH the new behavior and
|
||||
* parity with the old inline logic.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import type { InterceptedRequest } from "../../src/mitm/inspector/types.ts";
|
||||
|
||||
const { matchesTrafficFilter } = await import("../../src/lib/inspector/matchesTrafficFilter.ts");
|
||||
|
||||
function mkReq(partial: Partial<InterceptedRequest>): InterceptedRequest {
|
||||
return {
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
source: "agent-bridge",
|
||||
timestamp: "2026-06-17T00:00:00.000Z",
|
||||
method: "POST",
|
||||
host: "api.openai.com",
|
||||
path: "/v1/chat/completions",
|
||||
requestHeaders: {},
|
||||
requestBody: null,
|
||||
requestSize: 0,
|
||||
responseHeaders: {},
|
||||
responseBody: null,
|
||||
responseSize: 0,
|
||||
status: 200,
|
||||
detectedKind: "llm",
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
test("liveOnly keeps in-flight requests and drops finished ones", () => {
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: "in-flight" }), { liveOnly: true }), true);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: 200 }), { liveOnly: true }), false);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: "error" }), { liveOnly: true }), false);
|
||||
});
|
||||
|
||||
test("liveOnly off (or unset) does not filter by in-flight", () => {
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: 200 }), {}), true);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: 200 }), { liveOnly: false }), true);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: "in-flight" }), {}), true);
|
||||
});
|
||||
|
||||
test("liveOnly composes with other filters (host must still match)", () => {
|
||||
const req = mkReq({ status: "in-flight", host: "api.anthropic.com" });
|
||||
assert.equal(matchesTrafficFilter(req, { liveOnly: true, host: "openai" }), false);
|
||||
assert.equal(matchesTrafficFilter(req, { liveOnly: true, host: "anthropic" }), true);
|
||||
});
|
||||
|
||||
// ── Parity with the previous inline applyFilter logic ────────────────────────
|
||||
|
||||
test("profile=llm drops non-llm requests", () => {
|
||||
assert.equal(matchesTrafficFilter(mkReq({ detectedKind: "app" }), { profile: "llm" }), false);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ detectedKind: "llm" }), { profile: "llm" }), true);
|
||||
});
|
||||
|
||||
test("profile=custom keeps only custom-host source", () => {
|
||||
assert.equal(matchesTrafficFilter(mkReq({ source: "agent-bridge" }), { profile: "custom" }), false);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ source: "custom-host" }), { profile: "custom" }), true);
|
||||
});
|
||||
|
||||
test("host filter is a substring match", () => {
|
||||
assert.equal(matchesTrafficFilter(mkReq({ host: "api.openai.com" }), { host: "openai" }), true);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ host: "api.openai.com" }), { host: "google" }), false);
|
||||
});
|
||||
|
||||
test("agent, source, sessionId and sameContextKey filters", () => {
|
||||
assert.equal(matchesTrafficFilter(mkReq({ agent: "claude-code" }), { agent: "codex" }), false);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ source: "http-proxy" }), { source: "system-proxy" }), false);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ sessionId: "a" }), { sessionId: "b" }), false);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ contextKey: "abc" }), { sameContextKey: "xyz" }), false);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ contextKey: "abc" }), { sameContextKey: "abc" }), true);
|
||||
});
|
||||
|
||||
test("status category filter (2xx/4xx/5xx/error)", () => {
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: 200 }), { status: "2xx" }), true);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: 404 }), { status: "2xx" }), false);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: 503 }), { status: "5xx" }), true);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: "error" }), { status: "error" }), true);
|
||||
assert.equal(matchesTrafficFilter(mkReq({ status: 200 }), { status: "error" }), false);
|
||||
});
|
||||
|
||||
test("an empty filter set matches everything", () => {
|
||||
assert.equal(matchesTrafficFilter(mkReq({}), {}), true);
|
||||
});
|
||||
Reference in New Issue
Block a user