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

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;
}