Give the initial-load baseline a repeatable probe that strips query strings and can assert the login member fanout stays on the current project. Co-authored-by: Cursor <cursoragent@cursor.com>
64 lines
2.0 KiB
TypeScript
64 lines
2.0 KiB
TypeScript
/** 首次加载证据的纯函数。URL 查询串可能带会话参数,证据里只留路径。 */
|
|
|
|
export interface ResourceSample {
|
|
path: string;
|
|
initiator: string;
|
|
durationMs: number;
|
|
transferBytes: number;
|
|
decodedBytes: number;
|
|
}
|
|
|
|
export interface RequestSample {
|
|
method: string;
|
|
path: string;
|
|
}
|
|
|
|
const HEAVY_CHUNK = /katex|mermaid|docx|xlsx|AdminConsole|BlockEditor|FilePreview/i;
|
|
|
|
export function redactResourceUrl(raw: string): string {
|
|
const noQuery = raw.split('?')[0] ?? raw;
|
|
try {
|
|
const parsed = new URL(noQuery);
|
|
return parsed.pathname || '/';
|
|
} catch {
|
|
return noQuery.replace(/^https?:\/\/[^/]+/i, '') || '/';
|
|
}
|
|
}
|
|
|
|
export function percentile(values: number[], p: number): number | null {
|
|
if (values.length === 0) return null;
|
|
const sorted = [...values].sort((a, b) => a - b);
|
|
const rank = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
|
|
return sorted[rank] ?? null;
|
|
}
|
|
|
|
export function summarizeResources(resources: ResourceSample[]) {
|
|
const transferBytes = resources.reduce((sum, row) => sum + (row.transferBytes || 0), 0);
|
|
const js = resources.filter((row) => row.path.endsWith('.js'));
|
|
const heavy = resources.filter((row) => HEAVY_CHUNK.test(row.path)).map((row) => row.path);
|
|
return {
|
|
requestCount: resources.length,
|
|
transferBytes,
|
|
jsTransferBytes: js.reduce((sum, row) => sum + (row.transferBytes || 0), 0),
|
|
jsCount: js.length,
|
|
heavyChunks: [...new Set(heavy)],
|
|
};
|
|
}
|
|
|
|
export function countInitRequests(requests: RequestSample[]) {
|
|
const members = requests.filter(
|
|
(row) => row.method === 'GET' && /^\/api\/v1\/projects\/\d+\/members$/.test(row.path),
|
|
);
|
|
const sessions = requests.filter(
|
|
(row) => row.method === 'GET' && row.path === '/api/v1/chat/sessions',
|
|
);
|
|
const projectIds = new Set(
|
|
members.map((row) => row.path.split('/')[4]).filter((id): id is string => Boolean(id)),
|
|
);
|
|
return {
|
|
memberGetCount: members.length,
|
|
distinctMemberProjects: projectIds.size,
|
|
sessionListGetCount: sessions.length,
|
|
};
|
|
}
|