You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
4.1 KiB
153 lines
4.1 KiB
import fs from "node:fs";
|
|
import type { Page } from "playwright";
|
|
|
|
export type NetworkEvent = {
|
|
ts: number;
|
|
iso: string;
|
|
phase: string;
|
|
kind: "request" | "response";
|
|
method: string;
|
|
url: string;
|
|
status?: number;
|
|
resourceType?: string;
|
|
isPlaceCommit: boolean;
|
|
isLocationSearch: boolean;
|
|
isQuote: boolean;
|
|
};
|
|
|
|
export type CommitMoment = {
|
|
ts: number;
|
|
iso: string;
|
|
url: string;
|
|
status: number;
|
|
method: string;
|
|
msSinceRecordingStart: number;
|
|
msSinceLastCheckpoint: number | null;
|
|
checkpointPhase: string;
|
|
};
|
|
|
|
export class NetworkLogger {
|
|
private readonly events: NetworkEvent[] = [];
|
|
|
|
private readonly placeCommits: CommitMoment[] = [];
|
|
|
|
private recordingStartedAt = Date.now();
|
|
|
|
private lastCheckpointAt: number | null = null;
|
|
|
|
private checkpointPhase = "recording_start";
|
|
|
|
constructor(
|
|
private readonly outPath: string,
|
|
private readonly onPlaceCommit?: (moment: CommitMoment) => void,
|
|
) {
|
|
fs.mkdirSync(pathDir(outPath), { recursive: true });
|
|
}
|
|
|
|
setCheckpoint(phase: string): void {
|
|
this.checkpointPhase = phase;
|
|
this.lastCheckpointAt = Date.now();
|
|
}
|
|
|
|
attach(page: Page): void {
|
|
page.on("request", (req) => {
|
|
const url = req.url();
|
|
if (!this.isAxelRelevant(url)) {
|
|
return;
|
|
}
|
|
this.append({
|
|
ts: Date.now(),
|
|
iso: new Date().toISOString(),
|
|
phase: this.checkpointPhase,
|
|
kind: "request",
|
|
method: req.method(),
|
|
url: url.slice(0, 500),
|
|
resourceType: req.resourceType(),
|
|
isPlaceCommit: this.isPlaceCommitUrl(url),
|
|
isLocationSearch: url.includes("/axel/location/search"),
|
|
isQuote: url.includes("/axel/quote"),
|
|
});
|
|
});
|
|
|
|
page.on("response", async (res) => {
|
|
const url = res.url();
|
|
if (!this.isAxelRelevant(url)) {
|
|
return;
|
|
}
|
|
const status = res.status();
|
|
this.append({
|
|
ts: Date.now(),
|
|
iso: new Date().toISOString(),
|
|
phase: this.checkpointPhase,
|
|
kind: "response",
|
|
method: res.request().method(),
|
|
url: url.slice(0, 500),
|
|
status,
|
|
resourceType: res.request().resourceType(),
|
|
isPlaceCommit: this.isPlaceCommitUrl(url),
|
|
isLocationSearch: url.includes("/axel/location/search"),
|
|
isQuote: url.includes("/axel/quote"),
|
|
});
|
|
|
|
if (
|
|
this.isPlaceCommitUrl(url) &&
|
|
status === 200 &&
|
|
res.request().method().toUpperCase() === "GET"
|
|
) {
|
|
const now = Date.now();
|
|
const moment: CommitMoment = {
|
|
ts: now,
|
|
iso: new Date(now).toISOString(),
|
|
url: url.slice(0, 500),
|
|
status,
|
|
method: "GET",
|
|
msSinceRecordingStart: now - this.recordingStartedAt,
|
|
msSinceLastCheckpoint:
|
|
this.lastCheckpointAt === null ? null : now - this.lastCheckpointAt,
|
|
checkpointPhase: this.checkpointPhase,
|
|
};
|
|
this.placeCommits.push(moment);
|
|
this.onPlaceCommit?.(moment);
|
|
console.log(
|
|
`[录证] ★ place commit ${status} @ +${moment.msSinceRecordingStart}ms phase=${this.checkpointPhase}`,
|
|
);
|
|
console.log(` ${url.slice(0, 120)}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
flush(): void {
|
|
fs.writeFileSync(this.outPath, this.events.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
|
|
}
|
|
|
|
getPlaceCommits(): CommitMoment[] {
|
|
return [...this.placeCommits];
|
|
}
|
|
|
|
private isAxelRelevant(url: string): boolean {
|
|
return (
|
|
url.includes("services.mothership.com/axel/location") ||
|
|
url.includes("services.mothership.com/axel/quote") ||
|
|
url.includes("maps.googleapis.com") ||
|
|
url.includes("places.googleapis.com")
|
|
);
|
|
}
|
|
|
|
private isPlaceCommitUrl(url: string): boolean {
|
|
return (
|
|
url.includes("/axel/location/place/") &&
|
|
!url.includes("reverse-geocode")
|
|
);
|
|
}
|
|
|
|
private append(event: NetworkEvent): void {
|
|
this.events.push(event);
|
|
fs.appendFileSync(this.outPath, `${JSON.stringify(event)}\n`, "utf8");
|
|
}
|
|
}
|
|
|
|
function pathDir(filePath: string): string {
|
|
const idx = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
|
|
return idx >= 0 ? filePath.slice(0, idx) : filePath;
|
|
}
|