136 lines
4.9 KiB
JavaScript
136 lines
4.9 KiB
JavaScript
import React, { memo, useMemo, useState } from "react";
|
||
|
||
function prettyJson(text) {
|
||
if (!text) return "—";
|
||
try {
|
||
return JSON.stringify(JSON.parse(text), null, 2);
|
||
} catch {
|
||
return text;
|
||
}
|
||
}
|
||
|
||
function ApiTraceTab({
|
||
hasSelectedTask,
|
||
selectedTaskName,
|
||
apiTraceLogs,
|
||
formatTimestamp,
|
||
clearApiTrace,
|
||
exportApiTraceJson,
|
||
exportApiTraceCsv
|
||
}) {
|
||
const [search, setSearch] = useState("");
|
||
const query = String(search || "").trim().toLowerCase();
|
||
const allRows = Array.isArray(apiTraceLogs) ? apiTraceLogs : [];
|
||
|
||
const rows = useMemo(() => {
|
||
if (!query) return allRows;
|
||
return allRows.filter((item) => {
|
||
const hay = [
|
||
item.method,
|
||
item.phone,
|
||
item.errorText,
|
||
item.requestJson,
|
||
item.responseJson
|
||
].map((part) => String(part || "").toLowerCase()).join(" ");
|
||
return hay.includes(query);
|
||
});
|
||
}, [allRows, query]);
|
||
|
||
const okCount = rows.filter((item) => item.ok).length;
|
||
const failCount = rows.length - okCount;
|
||
const inviteRows = allRows.filter((item) => item.method === "channels.InviteToChannel");
|
||
const inviteTotal = inviteRows.length;
|
||
const inviteMissing = inviteRows.filter((item) => {
|
||
const response = String(item.responseJson || "");
|
||
return response.includes("missingInvitees") || response.includes("missing_invitees");
|
||
}).length;
|
||
const inviteRpcError = inviteRows.filter((item) => !item.ok || String(item.errorText || "").trim()).length;
|
||
const inviteOkNoMissing = Math.max(0, inviteTotal - inviteMissing - inviteRpcError);
|
||
|
||
return (
|
||
<section className="card">
|
||
<div className="row-header">
|
||
<h3>API трассировка</h3>
|
||
<div className="actions">
|
||
<button
|
||
type="button"
|
||
className="secondary"
|
||
onClick={() => exportApiTraceJson()}
|
||
>
|
||
Экспорт JSON
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="secondary"
|
||
onClick={() => exportApiTraceCsv()}
|
||
>
|
||
Экспорт CSV
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="secondary"
|
||
onClick={() => clearApiTrace()}
|
||
>
|
||
Очистить трассировку
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="hint">
|
||
Задача: {hasSelectedTask ? selectedTaskName : "—"}.
|
||
Для Telegram MTProto HTTP‑headers отсутствуют, поэтому в лог пишется транспортный контекст.
|
||
</div>
|
||
<div className="inline-controls">
|
||
<input
|
||
className="text-input"
|
||
value={search}
|
||
onChange={(event) => setSearch(event.target.value)}
|
||
placeholder="Поиск по методу / ошибке / JSON"
|
||
/>
|
||
<span className="status-caption">Всего: {rows.length}</span>
|
||
<span className="status-caption ok">OK: {okCount}</span>
|
||
<span className="status-caption warn">Ошибок: {failCount}</span>
|
||
</div>
|
||
<div className="inline-controls">
|
||
<span className="status-caption">InviteToChannel: {inviteTotal}</span>
|
||
<span className="status-caption warn">missing_invitees: {inviteMissing}</span>
|
||
<span className="status-caption ok">без missing: {inviteOkNoMissing}</span>
|
||
<span className="status-caption">RPC errors: {inviteRpcError}</span>
|
||
</div>
|
||
<div className="api-trace-list">
|
||
{!rows.length && <div className="empty">Трассировка пуста.</div>}
|
||
{rows.map((item) => (
|
||
<details key={item.id} className={`api-trace-item ${item.ok ? "success" : "error"}`} open={false}>
|
||
<summary>
|
||
<div className="api-trace-head">
|
||
<strong>{item.method || "unknown"}</strong>
|
||
<span className="status-caption">{formatTimestamp(item.createdAt)}</span>
|
||
</div>
|
||
<div className="api-trace-body">
|
||
<div>Аккаунт: {item.phone || item.accountId || "—"}</div>
|
||
<div>Задача: {item.taskId || "—"} · {item.ok ? "OK" : "Ошибка"} · {item.durationMs || 0} ms</div>
|
||
{!item.ok && <div>Ошибка: {item.errorText || "—"}</div>}
|
||
</div>
|
||
</summary>
|
||
<div className="api-trace-details">
|
||
<div>
|
||
<strong>Запрос (JSON)</strong>
|
||
<pre>{prettyJson(item.requestJson)}</pre>
|
||
</div>
|
||
<div>
|
||
<strong>Заголовки / контекст</strong>
|
||
<pre>{prettyJson(item.headersJson)}</pre>
|
||
</div>
|
||
<div>
|
||
<strong>Ответ (JSON)</strong>
|
||
<pre>{prettyJson(item.responseJson)}</pre>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
))}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
export default memo(ApiTraceTab);
|