|
|
import type { Page } from "playwright";
|
|
|
|
|
|
export type NetworkTraceEntry = {
|
|
|
kind: "req" | "resp";
|
|
|
url: string;
|
|
|
method?: string;
|
|
|
status?: number;
|
|
|
contentType?: string;
|
|
|
};
|
|
|
|
|
|
export function isNetworkVisibilityEnabled(): boolean {
|
|
|
return process.env.RPA_QUOTE_NETWORK_VISIBILITY === "true";
|
|
|
}
|
|
|
|
|
|
export type NetworkVisibilityHandle = {
|
|
|
trace: NetworkTraceEntry[];
|
|
|
detach: () => void;
|
|
|
logSummary: (label: string) => void;
|
|
|
};
|
|
|
|
|
|
/** submit 窗口内记录全部 REQ/RESP(RPA_QUOTE_NETWORK_VISIBILITY=true) */
|
|
|
export function attachNetworkVisibility(page: Page): NetworkVisibilityHandle {
|
|
|
const trace: NetworkTraceEntry[] = [];
|
|
|
const verbose = isNetworkVisibilityEnabled();
|
|
|
|
|
|
const onRequest = (request: {
|
|
|
url: () => string;
|
|
|
method: () => string;
|
|
|
}): void => {
|
|
|
const url = request.url();
|
|
|
const method = request.method();
|
|
|
trace.push({ kind: "req", url, method });
|
|
|
if (verbose) {
|
|
|
console.log(`[rpa] REQ: ${method} ${url}`);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const onResponse = (response: {
|
|
|
url: () => string;
|
|
|
status: () => number;
|
|
|
headers: () => Record<string, string>;
|
|
|
}): void => {
|
|
|
const url = response.url();
|
|
|
const status = response.status();
|
|
|
const contentType = response.headers()["content-type"] ?? "";
|
|
|
trace.push({ kind: "resp", url, status, contentType });
|
|
|
if (verbose) {
|
|
|
console.log(`[rpa] RESP: ${status} ${url} ct=${contentType}`);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
page.on("request", onRequest);
|
|
|
page.on("response", onResponse);
|
|
|
|
|
|
return {
|
|
|
trace,
|
|
|
detach: () => {
|
|
|
page.off("request", onRequest);
|
|
|
page.off("response", onResponse);
|
|
|
},
|
|
|
logSummary: (label: string) => {
|
|
|
const reqs = trace.filter((t) => t.kind === "req");
|
|
|
const resps = trace.filter((t) => t.kind === "resp");
|
|
|
const quoteLike = trace.filter(
|
|
|
(t) =>
|
|
|
/quote|rate|graphql|pricing|mothership\.com\/api/i.test(t.url) &&
|
|
|
!/segment|analytics|hotjar|sentry/i.test(t.url),
|
|
|
);
|
|
|
console.log(
|
|
|
`[rpa] quote-network ${label}: req=${reqs.length} resp=${resps.length} quoteLike=${quoteLike.length}`,
|
|
|
);
|
|
|
if (quoteLike.length > 0) {
|
|
|
for (const entry of quoteLike.slice(0, 30)) {
|
|
|
const extra =
|
|
|
entry.kind === "resp"
|
|
|
? ` status=${entry.status} ct=${entry.contentType ?? ""}`
|
|
|
: ` method=${entry.method ?? ""}`;
|
|
|
console.log(`[rpa] quote-network hit: ${entry.kind} ${entry.url}${extra}`);
|
|
|
}
|
|
|
} else if (verbose && trace.length > 0) {
|
|
|
for (const entry of trace.slice(-40)) {
|
|
|
console.log(
|
|
|
`[rpa] quote-network trace: ${entry.kind} ${entry.url}`,
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
};
|
|
|
}
|