@zintrust/trace 1.6.6 → 1.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -3
- package/src/TraceConnection.ts +0 -182
- package/src/cli-register.ts +0 -63
- package/src/config.ts +0 -383
- package/src/context.ts +0 -101
- package/src/dashboard/handlers.ts +0 -353
- package/src/dashboard/routes.ts +0 -114
- package/src/dashboard/ui.ts +0 -1262
- package/src/dashboard/zintrust-debuger.svg +0 -30
- package/src/index.ts +0 -102
- package/src/ingest/TraceIngestGateway.ts +0 -414
- package/src/plugin.ts +0 -9
- package/src/register.ts +0 -702
- package/src/storage/ProxyTraceStorage.ts +0 -190
- package/src/storage/TraceContentBudget.ts +0 -493
- package/src/storage/TraceContentRedaction.ts +0 -44
- package/src/storage/TraceEntryFiltering.ts +0 -50
- package/src/storage/TraceServiceTag.ts +0 -56
- package/src/storage/TraceStorage.ts +0 -543
- package/src/storage/TraceWriteDiagnostics.ts +0 -289
- package/src/storage/index.ts +0 -4
- package/src/types.ts +0 -430
- package/src/ui.ts +0 -9
- package/src/utils/authTag.ts +0 -20
- package/src/utils/entryFilter.ts +0 -131
- package/src/utils/familyHash.ts +0 -8
- package/src/utils/redact.ts +0 -112
- package/src/utils/requestFilter.ts +0 -79
- package/src/utils/stackFrame.ts +0 -44
- package/src/watchers/AuthWatcher.ts +0 -53
- package/src/watchers/BatchWatcher.ts +0 -55
- package/src/watchers/CacheWatcher.ts +0 -72
- package/src/watchers/CommandWatcher.ts +0 -58
- package/src/watchers/DumpWatcher.ts +0 -45
- package/src/watchers/EventWatcher.ts +0 -46
- package/src/watchers/ExceptionWatcher.ts +0 -130
- package/src/watchers/GateWatcher.ts +0 -53
- package/src/watchers/HttpClientWatcher.ts +0 -219
- package/src/watchers/HttpWatcher.ts +0 -249
- package/src/watchers/JobWatcher.ts +0 -124
- package/src/watchers/LogWatcher.ts +0 -120
- package/src/watchers/MailWatcher.ts +0 -65
- package/src/watchers/MiddlewareWatcher.ts +0 -54
- package/src/watchers/ModelWatcher.ts +0 -60
- package/src/watchers/NotificationWatcher.ts +0 -60
- package/src/watchers/QueryWatcher.ts +0 -105
- package/src/watchers/RedisWatcher.ts +0 -42
- package/src/watchers/ScheduleWatcher.ts +0 -57
- package/src/watchers/ViewWatcher.ts +0 -40
package/src/dashboard/ui.ts
DELETED
|
@@ -1,1262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Trace dashboard SPA — inline HTML served at basePath.
|
|
3
|
-
* Full REST API registered under basePath/api/*.
|
|
4
|
-
*/
|
|
5
|
-
const BRAND_SVG = `<svg width="120" height="120" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
6
|
-
<defs>
|
|
7
|
-
<linearGradient id="zt-trace-brand" x1="15" y1="15" x2="85" y2="85" gradientUnits="userSpaceOnUse">
|
|
8
|
-
<stop stop-color="#38bdf8" />
|
|
9
|
-
<stop offset="1" stop-color="#22c55e" />
|
|
10
|
-
</linearGradient>
|
|
11
|
-
</defs>
|
|
12
|
-
<path
|
|
13
|
-
d="M50 8L18 22V46C18 66.2 32 84.1 50 92C68 84.1 82 66.2 82 46V22L50 8Z"
|
|
14
|
-
stroke="url(#zt-trace-brand)"
|
|
15
|
-
stroke-width="6"
|
|
16
|
-
stroke-linejoin="round"
|
|
17
|
-
/>
|
|
18
|
-
<path
|
|
19
|
-
d="M34 54H42L46 44L52 62L58 50H66"
|
|
20
|
-
stroke="white"
|
|
21
|
-
stroke-width="8"
|
|
22
|
-
stroke-linecap="round"
|
|
23
|
-
stroke-linejoin="round"
|
|
24
|
-
/>
|
|
25
|
-
<circle cx="34" cy="54" r="2.8" fill="white" fill-opacity="0.7" />
|
|
26
|
-
<circle cx="66" cy="50" r="2.8" fill="white" fill-opacity="0.7" />
|
|
27
|
-
<path
|
|
28
|
-
d="M30 28H70"
|
|
29
|
-
stroke="white"
|
|
30
|
-
stroke-opacity="0.12"
|
|
31
|
-
stroke-width="3"
|
|
32
|
-
stroke-linecap="round"
|
|
33
|
-
/>
|
|
34
|
-
</svg>`;
|
|
35
|
-
|
|
36
|
-
const SUN_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2.2M12 19.8V22M4.93 4.93l1.56 1.56M17.51 17.51l1.56 1.56M2 12h2.2M19.8 12H22M4.93 19.07l1.56-1.56M17.51 6.49l1.56-1.56"></path></svg>`;
|
|
37
|
-
|
|
38
|
-
const MOON_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"></path></svg>`;
|
|
39
|
-
|
|
40
|
-
const COPY_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
|
|
41
|
-
const DISCLOSURE_ICON = `<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4l6 6-6 6"></path></svg>`;
|
|
42
|
-
|
|
43
|
-
const JSON_HIGHLIGHT_PATTERN = String.raw`("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")(?=\s*:)|(\s*:)|("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?`;
|
|
44
|
-
const SQL_HIGHLIGHT_PATTERN = String.raw`(\/\*[\s\S]*?\*\/|--.*$|'(?:''|[^'])*'|\x60[^\x60]+\x60|\b(?:select|from|where|insert|into|values|update|delete|join|left|right|inner|outer|on|and|or|limit|order|by|group|having|as|distinct|null|is|in|like|set|case|when|then|else|end|returning|union|all)\b|-?\d+(?:\.\d+)?)`;
|
|
45
|
-
|
|
46
|
-
const encodeSvgDataUri = (svg: string): string => {
|
|
47
|
-
const compactSvg = svg.replaceAll(/>\s+</g, '><').trim();
|
|
48
|
-
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(compactSvg)}`;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
52
|
-
<html lang="en">
|
|
53
|
-
<head>
|
|
54
|
-
<meta charset="UTF-8">
|
|
55
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
56
|
-
<meta name="color-scheme" content="dark light">
|
|
57
|
-
<title>__TRACE_TITLE__</title>
|
|
58
|
-
<link rel="icon" type="image/svg+xml" href="__TRACE_FAVICON__">
|
|
59
|
-
<script>
|
|
60
|
-
(function(){
|
|
61
|
-
const KEY = 'zintrust-trace-theme';
|
|
62
|
-
let theme = 'dark';
|
|
63
|
-
try {
|
|
64
|
-
const stored = window.localStorage.getItem(KEY);
|
|
65
|
-
if (stored === 'light' || stored === 'dark') theme = stored;
|
|
66
|
-
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) theme = 'light';
|
|
67
|
-
} catch {}
|
|
68
|
-
document.documentElement.dataset.theme = theme;
|
|
69
|
-
})();
|
|
70
|
-
</script>
|
|
71
|
-
<style>
|
|
72
|
-
:root{--bg:#0b1220;--surface:rgba(15,23,42,.82);--surface-strong:#13233b;--surface-soft:rgba(15,23,42,.56);--line:rgba(148,163,184,.18);--text:#e5edf8;--muted:#94a3b8;--accent:#38bdf8;--accent-strong:#0ea5e9;--success:#22c55e;--danger:#ef4444;--warn:#f59e0b;--code-bg:#06101f;--code-border:rgba(56,189,248,.14);--shadow:0 24px 70px rgba(2,8,23,.35);--font:'Inter',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;--mono:'SF Mono',SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;--radius:18px}
|
|
73
|
-
html[data-theme='light']{--bg:#f4f6fb;--surface:rgba(255,255,255,.94);--surface-strong:#ffffff;--surface-soft:rgba(255,255,255,.8);--line:#dde4ef;--text:#172033;--muted:#6d7890;--accent:#2563eb;--accent-strong:#1d4ed8;--success:#16a34a;--danger:#dc2626;--warn:#b45309;--code-bg:#f7fbff;--code-border:#d7e1ee;--shadow:0 20px 60px rgba(15,23,42,.08)}
|
|
74
|
-
*{box-sizing:border-box}html,body{margin:0;min-height:100%}body{min-height:100vh;background:linear-gradient(180deg,rgba(56,189,248,.1),transparent 220px),var(--bg);font-family:var(--font);color:var(--text)}button,input,select{font:inherit}
|
|
75
|
-
.layout{display:grid;grid-template-columns:300px minmax(0,1fr);min-height:100vh}.sidebar{padding:26px 18px 22px;background:var(--surface-soft);backdrop-filter:blur(14px);border-right:1px solid var(--line);position:sticky;top:0;height:100vh;overflow:auto;z-index:2}.brand-row{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:4px 10px 22px}.brand{display:flex;align-items:center;gap:12px;min-width:0}.brand-mark{width:44px;height:44px;border-radius:14px;border:1px solid rgba(56,189,248,.22);background:linear-gradient(180deg,rgba(56,189,248,.16),rgba(34,197,94,.12));display:grid;place-items:center;flex:none}.brand-mark svg{width:28px;height:28px;display:block}.brand-copy h1{margin:0;font-size:1.42rem;line-height:1.08}.brand-copy p{margin:4px 0 0;color:var(--muted);font-size:.92rem}.sidebar-status{margin:0 10px 18px;padding:12px 14px;border-radius:14px;border:1px solid var(--line);background:var(--surface);color:var(--muted);line-height:1.5}.sidebar-status strong{display:block;color:var(--text);font-size:.95rem;margin-bottom:4px}.sidebar-group{padding:0 8px;margin-top:8px}.sidebar-label{margin:0 0 10px;font-size:.74rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.nav-button{width:100%;display:flex;align-items:center;justify-content:space-between;gap:10px;border:none;border-radius:14px;padding:12px 14px;background:transparent;color:var(--muted);cursor:pointer;transition:background .16s ease,color .16s ease,box-shadow .16s ease;position:relative;z-index:1}.nav-button:hover,.nav-button:focus-visible{background:rgba(56,189,248,.1);color:var(--text);outline:none}.nav-button.active{background:rgba(56,189,248,.14);color:var(--text);box-shadow:inset 0 0 0 1px rgba(56,189,248,.22)}.nav-button+.nav-button{margin-top:6px}.nav-title{font-weight:700}.nav-meta{font-size:.8rem;opacity:.75}.icon-button{width:42px;height:42px;border-radius:12px;border:1px solid var(--line);background:var(--surface);color:var(--text);display:grid;place-items:center;cursor:pointer;transition:border-color .16s ease,transform .16s ease,color .16s ease}.icon-button:hover,.icon-button:focus-visible{border-color:rgba(56,189,248,.4);color:var(--accent);transform:translateY(-1px);outline:none}.icon-button svg{width:18px;height:18px;display:block}
|
|
76
|
-
.main{padding:28px;min-width:0}.shell{max-width:1320px;margin:0 auto;display:grid;gap:18px}
|
|
77
|
-
.panel{border-radius:var(--radius);border:1px solid var(--line);background:var(--surface);box-shadow:var(--shadow);backdrop-filter:blur(16px)}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:16px;margin-bottom:18px}.stat-card{padding:20px;position:relative;overflow:hidden}.stat-card::after{content:'';position:absolute;right:-18px;bottom:-26px;width:92px;height:92px;border-radius:28px;background:linear-gradient(135deg,rgba(56,189,248,.16),rgba(34,197,94,.08));transform:rotate(18deg)}.stat-label{font-size:.74rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800;margin-bottom:12px}.stat-value{font-size:2.25rem;font-weight:800;line-height:1}.stat-meta{margin-top:10px;color:var(--muted);font-size:.9rem}.content-grid{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(320px,.95fr);gap:18px}.side-stack{display:grid;gap:18px}
|
|
78
|
-
.section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:22px 24px 16px}.section-head h3{margin:0;font-size:1.04rem}.section-head p{margin:6px 0 0;color:var(--muted);font-size:.92rem}.toolbar{display:flex;flex-wrap:wrap;gap:10px;padding:0 24px 18px}.control,.toolbar input,.toolbar select{height:44px;border-radius:13px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);padding:0 14px;min-width:0}.toolbar input,.toolbar select{flex:1 1 180px}.toolbar input::placeholder{color:var(--muted)}.btn{height:44px;border:none;border-radius:13px;padding:0 16px;cursor:pointer;font-weight:800}.btn-primary{background:linear-gradient(135deg,var(--accent-strong),var(--accent));color:#fff}.btn-danger{background:rgba(239,68,68,.12);color:var(--danger);border:1px solid rgba(239,68,68,.18)}.btn-ghost{background:var(--surface-soft);color:var(--text);border:1px solid var(--line)}
|
|
79
|
-
.table-wrap{overflow:auto;padding:0 12px 12px}.table-wrap table{width:100%;border-collapse:separate;border-spacing:0;min-width:880px}th{padding:14px;color:var(--muted);font-size:.74rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;text-align:left;border-bottom:1px solid var(--line)}td{padding:15px 14px;border-bottom:1px solid var(--line);vertical-align:top}.row-button{cursor:pointer}.row-button:hover td{background:rgba(56,189,248,.05)}.summary{font-size:.93rem;font-weight:700;line-height:1.4;color:var(--text)}.summary-sub{margin-top:6px;color:var(--muted);font-size:.82rem;line-height:1.4}.mono{font-family:var(--mono)}.empty{padding:44px 24px;color:var(--muted);line-height:1.65;text-align:center}.pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 24px 24px;color:var(--muted);flex-wrap:wrap}.pagination-controls{display:flex;gap:8px}.pagination button{height:40px;min-width:92px;padding:0 14px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer}.pagination button:disabled{opacity:.45;cursor:not-allowed}
|
|
80
|
-
.activity-list{list-style:none;margin:0;padding:0 24px 24px}.activity-item{padding:14px 0;border-top:1px solid var(--line)}.activity-item:first-child{border-top:none}.activity-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.activity-time{color:var(--muted);font-size:.85rem}.activity-summary{margin-top:8px;color:var(--text);line-height:1.48}.back-link{display:inline-flex;align-items:center;gap:8px;margin:0 0 14px;color:var(--accent);font-weight:800;cursor:pointer}.detail-card{padding:24px}.detail-meta{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 20px;color:var(--muted);font-size:.9rem;overflow-wrap:anywhere}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.detail-stack{display:grid;gap:16px;margin-top:18px}.detail-box{padding:16px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.detail-box h4{margin:0 0 10px;font-size:.92rem}.detail-box dl{margin:0;display:grid;gap:8px}.detail-box dt{font-size:.76rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.detail-box dd{margin:0;color:var(--text);line-height:1.45;overflow-wrap:anywhere}.trace-tabs{display:flex;gap:10px;flex-wrap:wrap;margin:20px 0 16px}.trace-tab{border:none;border-radius:12px;padding:10px 12px;background:transparent;color:var(--muted);cursor:pointer;box-shadow:inset 0 0 0 1px var(--line);font-weight:800}.trace-tab.active{background:rgba(56,189,248,.12);color:var(--text);box-shadow:inset 0 0 0 1px rgba(56,189,248,.28)}.trace-panel{display:grid;gap:14px}.trace-item{padding:18px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.trace-item-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.trace-item-summary{margin-top:10px;display:grid;gap:10px}.trace-note{color:var(--muted);line-height:1.6}.trace-disclosure{padding:0;overflow:hidden}.trace-disclosure[open]{padding-bottom:18px}.trace-disclosure .trace-item-summary{margin-top:0}.trace-disclosure-body{display:grid;gap:12px;padding:0 18px}.trace-summary{list-style:none;cursor:pointer;padding:18px}.trace-summary::-webkit-details-marker{display:none}.trace-summary-main{display:grid;gap:10px;min-width:0;flex:1}.trace-summary-copy{display:grid;gap:6px;min-width:0}.trace-summary-copy .summary,.trace-summary-copy .summary-sub{display:block;overflow-wrap:anywhere}.trace-disclosure-body .summary-sub{overflow-wrap:anywhere}
|
|
81
|
-
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18);text-decoration:none}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill,.status-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-request.method-get{background:rgba(34,197,94,.16);color:#bbf7d0}.pill-request.method-post{background:rgba(59,130,246,.16);color:#bfdbfe}.pill-request.method-other{background:rgba(245,158,11,.16);color:#fde68a}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}.status-pill{white-space:nowrap}.status-pill.status-2xx{background:rgba(34,197,94,.16);color:#bbf7d0;border-color:rgba(34,197,94,.24)}.status-pill.status-4xx{background:rgba(245,158,11,.16);color:#fde68a;border-color:rgba(245,158,11,.24)}.status-pill.status-5xx{background:rgba(239,68,68,.16);color:#fecaca;border-color:rgba(239,68,68,.24)}.status-pill.status-other{background:rgba(148,163,184,.14);color:#e2e8f0;border-color:rgba(148,163,184,.2)}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-request.method-get{color:#166534}html[data-theme='light'] .pill-request.method-post{color:#1d4ed8}html[data-theme='light'] .pill-request.method-other{color:#92400e}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}html[data-theme='light'] .status-pill.status-2xx{color:#166534}html[data-theme='light'] .status-pill.status-4xx{color:#92400e}html[data-theme='light'] .status-pill.status-5xx{color:#b91c1c}html[data-theme='light'] .status-pill.status-other{color:#334155}
|
|
82
|
-
.monitoring-wrap{padding:0 24px 24px}.tag-list{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.tag-item{display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:999px;border:1px solid var(--line);background:var(--surface-strong)}.tag-remove{border:none;background:rgba(239,68,68,.14);color:var(--danger);border-radius:999px;width:24px;height:24px;cursor:pointer;font-size:1rem;line-height:1}.helper-text{color:var(--muted);line-height:1.6}
|
|
83
|
-
.duration-chip{display:inline-flex;align-items:center;padding:5px 9px;border-radius:999px;border:1px solid transparent;font-size:.8rem;font-weight:700;color:var(--text);white-space:nowrap}.duration-chip.vfast{background:rgba(34,197,94,.14);border-color:rgba(34,197,94,.28);color:#bbf7d0}.duration-chip.fast{background:rgba(56,189,248,.12);border-color:rgba(56,189,248,.24);color:#bae6fd}.duration-chip.slow{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#fde68a}.duration-chip.vslow{background:rgba(239,68,68,.14);border-color:rgba(239,68,68,.24);color:#fecaca}html[data-theme='light'] .duration-chip.vfast{color:#166534}html[data-theme='light'] .duration-chip.fast{color:#1d4ed8}html[data-theme='light'] .duration-chip.slow{color:#92400e}html[data-theme='light'] .duration-chip.vslow{color:#b91c1c}
|
|
84
|
-
.code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block.wrap{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.code-block code{font-family:inherit}.html-preview-wrap{padding:14px;background:var(--surface-strong);border-top:1px solid var(--line)}.html-preview{display:block;width:100%;min-height:320px;border:1px solid var(--line);border-radius:14px;background:#fff}.inline-collapse{margin:0;border-top:1px solid var(--line);background:var(--surface-strong)}.inline-collapse summary{cursor:pointer;list-style:none;padding:14px 16px;font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.inline-collapse summary::-webkit-details-marker{display:none}.inline-collapse[open] summary{border-bottom:1px solid var(--line)}.inline-collapse .code-block{border-top:none}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
|
|
85
|
-
@media (max-width:1120px){.content-grid{grid-template-columns:1fr}}@media (max-width:920px){.layout{grid-template-columns:1fr}.sidebar{position:static;height:auto;border-right:none;border-bottom:1px solid var(--line);padding:20px 16px 18px}.brand-row{padding:0 0 16px}.sidebar-status{margin:0 0 16px}.sidebar-group{padding:0}.main{padding:20px}}@media (max-width:760px){.main{padding:16px}.shell{gap:14px}.stat-value{font-size:1.9rem}.section-head{flex-direction:column;align-items:stretch}.section-head .btn{width:100%}.toolbar{display:grid;grid-template-columns:1fr;padding-left:18px;padding-right:18px}.toolbar .btn,.toolbar input,.toolbar select{width:100%;flex:none}.pagination{padding-left:18px;padding-right:18px}.pagination,.pagination-controls{width:100%}.pagination-controls{justify-content:space-between}.pagination button{flex:1 1 0}.detail-meta{display:grid;gap:8px}.trace-tabs{flex-wrap:nowrap;overflow:auto;padding-bottom:4px}.trace-tab{white-space:nowrap}.code-toolbar{flex-wrap:wrap}.copy-button{width:100%}.html-preview{min-height:220px}.table-wrap{padding:0 14px 14px;overflow:visible}.table-wrap table{min-width:0;border-spacing:0}.table-wrap thead{display:none}.table-wrap tbody{display:grid;gap:12px}.table-wrap tr{display:block;border:1px solid var(--line);border-radius:16px;background:var(--surface-soft);overflow:hidden}.table-wrap td{display:grid;grid-template-columns:minmax(84px,108px) minmax(0,1fr);gap:10px;align-items:start;padding:12px 14px;border-bottom:1px solid var(--line)}.table-wrap td::before{content:attr(data-label);font-size:.72rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}.table-wrap td:last-child{border-bottom:none}.table-wrap td[data-label='Summary'],.table-wrap td[data-label='Tags']{grid-template-columns:1fr}.table-wrap td[data-label='Summary']::before,.table-wrap td[data-label='Tags']::before{margin-bottom:2px}.row-button:hover td{background:transparent}.row-button:hover{box-shadow:inset 0 0 0 1px rgba(56,189,248,.18)}}@media (max-width:640px){.stats-grid{grid-template-columns:1fr}.detail-card{padding:18px}.toolbar,.section-head,.activity-list,.monitoring-wrap{padding-left:18px;padding-right:18px}.brand-row{align-items:stretch;gap:14px;padding:0 0 14px}.brand{width:100%;align-items:flex-start}.brand-copy{min-width:0}.brand-copy h1{font-size:1.18rem;line-height:1.12}.brand-copy p{font-size:.82rem;overflow-wrap:anywhere}.icon-button{align-self:flex-end}.sidebar-status{padding:12px}.nav-button{padding:11px 12px}.nav-title{font-size:.95rem}.nav-meta{font-size:.72rem}}@media (max-width:480px){.brand-row{flex-direction:column}.icon-button{align-self:flex-start}.nav-button{align-items:flex-start;flex-direction:column}.nav-meta{font-size:.7rem}}
|
|
86
|
-
</style>
|
|
87
|
-
</head>
|
|
88
|
-
<body>
|
|
89
|
-
<div class="layout">
|
|
90
|
-
<aside class="sidebar">
|
|
91
|
-
<div class="brand-row">
|
|
92
|
-
<div class="brand">
|
|
93
|
-
<div class="brand-mark">__TRACE_LOGO__</div>
|
|
94
|
-
<div class="brand-copy">
|
|
95
|
-
<h1>ZinTrust Trace</h1>
|
|
96
|
-
<p class="mono">__TRACE_PROJECT_NAME__</p>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
<button type="button" class="icon-button" id="theme-toggle" aria-label="Toggle theme"></button>
|
|
100
|
-
</div>
|
|
101
|
-
<div class="sidebar-status">
|
|
102
|
-
<strong id="page-title">Runtime overview</strong>
|
|
103
|
-
<span id="page-subtitle">Recent trace activity and trace filters.</span>
|
|
104
|
-
</div>
|
|
105
|
-
<div class="sidebar-group">
|
|
106
|
-
<p class="sidebar-label">Navigation</p>
|
|
107
|
-
<button type="button" class="nav-button active" data-page="overview"><span class="nav-title">Overview</span><span class="nav-meta">Summary</span></button>
|
|
108
|
-
<button type="button" class="nav-button" data-page="entries"><span class="nav-title">Entries</span><span class="nav-meta">Events</span></button>
|
|
109
|
-
<button type="button" class="nav-button" data-page="monitoring"><span class="nav-title">Monitoring</span><span class="nav-meta">Tags</span></button>
|
|
110
|
-
</div>
|
|
111
|
-
<div class="sidebar-group">
|
|
112
|
-
<p class="sidebar-label">Streams</p>
|
|
113
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="request"><span class="nav-title">Requests</span><span class="nav-meta">HTTP</span></button>
|
|
114
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="query"><span class="nav-title">Queries</span><span class="nav-meta">SQL</span></button>
|
|
115
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="job"><span class="nav-title">Jobs</span><span class="nav-meta">Queue</span></button>
|
|
116
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="exception"><span class="nav-title">Exceptions</span><span class="nav-meta">Errors</span></button>
|
|
117
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="log"><span class="nav-title">Logs</span><span class="nav-meta">App</span></button>
|
|
118
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="cache"><span class="nav-title">Cache</span><span class="nav-meta">Store</span></button>
|
|
119
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="client_request"><span class="nav-title">Http Client</span><span class="nav-meta">Outbound</span></button>
|
|
120
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="redis"><span class="nav-title">Redis</span><span class="nav-meta">Command</span></button>
|
|
121
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="notification"><span class="nav-title">Notifications</span><span class="nav-meta">Alerts</span></button>
|
|
122
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="model"><span class="nav-title">Models</span><span class="nav-meta">ORM</span></button>
|
|
123
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="schedule"><span class="nav-title">Schedule</span><span class="nav-meta">Tasks</span></button>
|
|
124
|
-
<button type="button" class="nav-button" data-action="type-shortcut" data-type="mail"><span class="nav-title">Mail</span><span class="nav-meta">Outbound</span></button>
|
|
125
|
-
</div>
|
|
126
|
-
</aside>
|
|
127
|
-
<main class="main">
|
|
128
|
-
<div class="shell">
|
|
129
|
-
<div id="main"></div>
|
|
130
|
-
</div>
|
|
131
|
-
</main>
|
|
132
|
-
</div>
|
|
133
|
-
<script>
|
|
134
|
-
(function(){
|
|
135
|
-
const BASE = __TRACE_BASE_PATH_JSON__;
|
|
136
|
-
const API = BASE + '/api';
|
|
137
|
-
const THEME_KEY = 'zintrust-trace-theme';
|
|
138
|
-
const SUN_ICON = __TRACE_SUN_ICON__;
|
|
139
|
-
const MOON_ICON = __TRACE_MOON_ICON__;
|
|
140
|
-
const COPY_ICON = __TRACE_COPY_ICON__;
|
|
141
|
-
const DISCLOSURE_ICON = __TRACE_DISCLOSURE_ICON__;
|
|
142
|
-
const JSON_HIGHLIGHT_PATTERN = new RegExp(__TRACE_JSON_REGEX__, 'g');
|
|
143
|
-
const SQL_HIGHLIGHT_PATTERN = new RegExp(__TRACE_SQL_REGEX__, 'gim');
|
|
144
|
-
const ENTRY_TYPES = ['request','query','exception','log','job','cache','schedule','mail','auth','event','model','notification','redis','gate','middleware','command','batch','dump','view','client_request'];
|
|
145
|
-
const PAGE_COPY = {
|
|
146
|
-
overview: { title: 'Runtime overview', subtitle: 'Recent trace activity and trace filters.' },
|
|
147
|
-
entries: { title: 'Entry explorer', subtitle: 'Filter by type, tag, or batch.' },
|
|
148
|
-
monitoring: { title: 'Monitoring tags', subtitle: 'Pinned tags for trace pivots.' }
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const createInitialState = () => {
|
|
152
|
-
const search = new URLSearchParams(window.location.search);
|
|
153
|
-
const page = search.get('page');
|
|
154
|
-
const entriesPage = Number.parseInt(search.get('entriesPage') || '1', 10);
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
page: page && Object.prototype.hasOwnProperty.call(PAGE_COPY, page) ? page : 'overview',
|
|
158
|
-
entriesPage: Number.isFinite(entriesPage) && entriesPage > 0 ? entriesPage : 1,
|
|
159
|
-
entriesFilter: {
|
|
160
|
-
type: search.get('type') || '',
|
|
161
|
-
tag: search.get('tag') || '',
|
|
162
|
-
batchId: search.get('batchId') || ''
|
|
163
|
-
},
|
|
164
|
-
detail: null,
|
|
165
|
-
detailBatch: null,
|
|
166
|
-
detailTab: 'summary'
|
|
167
|
-
};
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
let state = createInitialState();
|
|
171
|
-
|
|
172
|
-
const DETAIL_BATCH_PAGE_SIZE = 10;
|
|
173
|
-
const DETAIL_BATCH_SCOPE_BY_TAB = Object.freeze({
|
|
174
|
-
queries: { type: 'query' },
|
|
175
|
-
middleware: { type: 'middleware' },
|
|
176
|
-
models: { type: 'model' },
|
|
177
|
-
logs: { type: 'log' },
|
|
178
|
-
exceptions: { type: 'exception' },
|
|
179
|
-
http: { type: 'client_request' },
|
|
180
|
-
cache: { type: 'cache' },
|
|
181
|
-
other: { scope: 'other' }
|
|
182
|
-
});
|
|
183
|
-
const DETAIL_BATCH_COUNT_TYPES = Object.freeze({
|
|
184
|
-
queries: 'query',
|
|
185
|
-
middleware: 'middleware',
|
|
186
|
-
models: 'model',
|
|
187
|
-
logs: 'log',
|
|
188
|
-
exceptions: 'exception',
|
|
189
|
-
http: 'client_request',
|
|
190
|
-
cache: 'cache'
|
|
191
|
-
});
|
|
192
|
-
const DETAIL_BATCH_OTHER_EXCLUDED_TYPES = ['request','query','middleware','model','log','exception','client_request','cache'];
|
|
193
|
-
|
|
194
|
-
let copySequence = 0;
|
|
195
|
-
const copyPayloads = new Map();
|
|
196
|
-
|
|
197
|
-
const updateThemeButton = () => {
|
|
198
|
-
const theme = document.documentElement.dataset.theme === 'light' ? 'light' : 'dark';
|
|
199
|
-
const toggle = document.getElementById('theme-toggle');
|
|
200
|
-
if (toggle) {
|
|
201
|
-
toggle.innerHTML = theme === 'dark' ? SUN_ICON : MOON_ICON;
|
|
202
|
-
toggle.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const setTheme = (theme) => {
|
|
207
|
-
document.documentElement.dataset.theme = theme;
|
|
208
|
-
try { window.localStorage.setItem(THEME_KEY, theme); } catch {}
|
|
209
|
-
updateThemeButton();
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const setPageCopy = (page) => {
|
|
213
|
-
const copy = PAGE_COPY[page] || PAGE_COPY.overview;
|
|
214
|
-
const title = document.getElementById('page-title');
|
|
215
|
-
const subtitle = document.getElementById('page-subtitle');
|
|
216
|
-
if (title) title.textContent = copy.title;
|
|
217
|
-
if (subtitle) subtitle.textContent = copy.subtitle;
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const activeEntryShortcut = () => state.page === 'entries' && state.entriesFilter.type !== '' ? state.entriesFilter.type : '';
|
|
221
|
-
|
|
222
|
-
const escapeHtml = (value) => String(value ?? '')
|
|
223
|
-
.replace(/&/g, '&')
|
|
224
|
-
.replace(/</g, '<')
|
|
225
|
-
.replace(/>/g, '>')
|
|
226
|
-
.replace(/"/g, '"')
|
|
227
|
-
.replace(/'/g, ''');
|
|
228
|
-
|
|
229
|
-
const looksLikeHtml = (value) => new RegExp('</?(?:html|body|div|table)\\b|<!doctype\\b', 'i').test(String(value || ''));
|
|
230
|
-
|
|
231
|
-
const api = async (path, opts) => {
|
|
232
|
-
const response = await fetch(API + path, opts);
|
|
233
|
-
if (!response.ok) {
|
|
234
|
-
const errorBody = await response.json().catch(() => ({}));
|
|
235
|
-
throw new Error(errorBody.error || response.statusText);
|
|
236
|
-
}
|
|
237
|
-
return response.json();
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const requestMethodClass = (entry) => {
|
|
241
|
-
if (!entry || entry.type !== 'request') return '';
|
|
242
|
-
const method = String(entry.content && entry.content.method || '').toUpperCase();
|
|
243
|
-
if (method === 'GET') return ' method-get';
|
|
244
|
-
if (method === 'POST') return ' method-post';
|
|
245
|
-
return ' method-other';
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const normalizeMethodLabel = (value) => {
|
|
249
|
-
const method = String(value || '').trim().toUpperCase();
|
|
250
|
-
if (method === '') return 'Request';
|
|
251
|
-
return method.charAt(0) + method.slice(1).toLowerCase();
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const entryTypeLabel = (entry) => {
|
|
255
|
-
if (entry && entry.type === 'request') {
|
|
256
|
-
return normalizeMethodLabel(entry.content && entry.content.method);
|
|
257
|
-
}
|
|
258
|
-
return String(entry && entry.type || '');
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const typeClass = (entryOrType, maybeEntry) => {
|
|
262
|
-
const entry = maybeEntry || (typeof entryOrType === 'object' ? entryOrType : null);
|
|
263
|
-
const type = entry && entry.type ? entry.type : entryOrType;
|
|
264
|
-
return 'type-pill pill-' + String(type || '').replace(/_/g, '-') + requestMethodClass(entry);
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const statusToneClass = (value) => {
|
|
268
|
-
const status = Number(value);
|
|
269
|
-
if (!Number.isFinite(status)) return 'status-other';
|
|
270
|
-
if (status >= 500 && status < 600) return 'status-5xx';
|
|
271
|
-
if (status >= 400 && status < 500) return 'status-4xx';
|
|
272
|
-
if (status >= 200 && status < 300) return 'status-2xx';
|
|
273
|
-
return 'status-other';
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
const statusBadgeHtml = (value) => {
|
|
277
|
-
const status = Number(value);
|
|
278
|
-
if (!Number.isFinite(status)) return '';
|
|
279
|
-
return '<span class="status-pill ' + statusToneClass(status) + '">' + escapeHtml(String(status)) + '</span>';
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
const timeSince = (value) => {
|
|
283
|
-
const createdAt = Number(value);
|
|
284
|
-
if (!Number.isFinite(createdAt)) return 'Unknown';
|
|
285
|
-
const seconds = Math.max(0, Math.floor((Date.now() - createdAt) / 1000));
|
|
286
|
-
if (seconds < 60) return seconds + 's ago';
|
|
287
|
-
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
|
|
288
|
-
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago';
|
|
289
|
-
return Math.floor(seconds / 86400) + 'd ago';
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
const formatDuration = (value) => {
|
|
293
|
-
const duration = Number(value);
|
|
294
|
-
if (!Number.isFinite(duration) || duration < 0) return '-';
|
|
295
|
-
if (duration === 0) return '0 ms';
|
|
296
|
-
if (duration < 10) {
|
|
297
|
-
const fixed = duration.toFixed(2);
|
|
298
|
-
const compact = fixed.endsWith('.00') ? fixed.slice(0, -3) : fixed.endsWith('0') ? fixed.slice(0, -1) : fixed;
|
|
299
|
-
return compact + ' ms';
|
|
300
|
-
}
|
|
301
|
-
if (duration < 100) {
|
|
302
|
-
const fixed = duration.toFixed(1);
|
|
303
|
-
return (fixed.endsWith('.0') ? fixed.slice(0, -2) : fixed) + ' ms';
|
|
304
|
-
}
|
|
305
|
-
return Math.round(duration) + ' ms';
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const getEntryDuration = (entry) => {
|
|
309
|
-
const content = entry && entry.content ? entry.content : {};
|
|
310
|
-
const primary = Number(content.duration);
|
|
311
|
-
if (Number.isFinite(primary)) return primary;
|
|
312
|
-
const fallback = Number(content.time);
|
|
313
|
-
return Number.isFinite(fallback) ? fallback : null;
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const getDurationTone = (duration) => {
|
|
317
|
-
if (!Number.isFinite(duration) || duration < 0) return '';
|
|
318
|
-
if (duration < 25) return 'vfast';
|
|
319
|
-
if (duration < 150) return 'fast';
|
|
320
|
-
if (duration < 600) return 'slow';
|
|
321
|
-
return 'vslow';
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
const durationHtml = (entry) => {
|
|
325
|
-
const duration = getEntryDuration(entry);
|
|
326
|
-
if (duration === null) return '<span class="activity-time">-</span>';
|
|
327
|
-
const tone = getDurationTone(duration);
|
|
328
|
-
const toneLabel = tone === 'vfast' ? 'VFast' : tone === 'fast' ? 'Fast' : tone === 'slow' ? 'Slow' : 'VSlow';
|
|
329
|
-
return '<span class="duration-chip ' + tone + '" title="' + toneLabel + '">' + escapeHtml(formatDuration(duration)) + '</span>';
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
const buildEntriesHref = (params) => {
|
|
333
|
-
const search = new URLSearchParams();
|
|
334
|
-
search.set('page', 'entries');
|
|
335
|
-
if (params.type) search.set('type', String(params.type));
|
|
336
|
-
if (params.tag) search.set('tag', String(params.tag));
|
|
337
|
-
if (params.batchId) search.set('batchId', String(params.batchId));
|
|
338
|
-
return BASE + '?' + search.toString();
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const tagsHtml = (tags) => (tags || []).map((tag) => {
|
|
342
|
-
const css = tag === 'failed' ? 'tag failed' : tag === 'slow' ? 'tag slow' : 'tag';
|
|
343
|
-
const href = buildEntriesHref({ tag });
|
|
344
|
-
return '<a class="' + css + '" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '" href="' + escapeHtml(href) + '">' + escapeHtml(tag) + '</a>';
|
|
345
|
-
}).join('');
|
|
346
|
-
|
|
347
|
-
const syncUrl = () => {
|
|
348
|
-
const search = new URLSearchParams();
|
|
349
|
-
if (state.page !== 'overview') search.set('page', state.page);
|
|
350
|
-
if (state.page === 'entries' || state.entriesFilter.type || state.entriesFilter.tag || state.entriesFilter.batchId) {
|
|
351
|
-
if (state.entriesFilter.type) search.set('type', state.entriesFilter.type);
|
|
352
|
-
if (state.entriesFilter.tag) search.set('tag', state.entriesFilter.tag);
|
|
353
|
-
if (state.entriesFilter.batchId) search.set('batchId', state.entriesFilter.batchId);
|
|
354
|
-
if (state.entriesPage > 1) search.set('entriesPage', String(state.entriesPage));
|
|
355
|
-
}
|
|
356
|
-
const nextUrl = search.toString() === '' ? BASE : BASE + '?' + search.toString();
|
|
357
|
-
window.history.replaceState(null, '', nextUrl);
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const batchSnippet = (batchId) => {
|
|
361
|
-
const raw = String(batchId || '');
|
|
362
|
-
return raw === '' ? '-' : escapeHtml(raw.slice(0, 8));
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
const createDetailBatchState = (payload, scope = '', loading = false) => ({
|
|
366
|
-
counts: payload && typeof payload.counts === 'object' && payload.counts !== null ? payload.counts : {},
|
|
367
|
-
entries: payload && Array.isArray(payload.entries) ? payload.entries : [],
|
|
368
|
-
total: payload && Number.isFinite(Number(payload.total)) ? Number(payload.total) : 0,
|
|
369
|
-
page: payload && Number.isFinite(Number(payload.page)) && Number(payload.page) > 0 ? Number(payload.page) : 1,
|
|
370
|
-
perPage: payload && Number.isFinite(Number(payload.perPage)) && Number(payload.perPage) > 0 ? Number(payload.perPage) : DETAIL_BATCH_PAGE_SIZE,
|
|
371
|
-
scope,
|
|
372
|
-
loading
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
const detailBatchState = () => {
|
|
376
|
-
const detailBatch = state.detailBatch;
|
|
377
|
-
if (!detailBatch || typeof detailBatch !== 'object' || Array.isArray(detailBatch)) {
|
|
378
|
-
return createDetailBatchState(null);
|
|
379
|
-
}
|
|
380
|
-
return createDetailBatchState(detailBatch, typeof detailBatch.scope === 'string' ? detailBatch.scope : '', detailBatch.loading === true);
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
const batchEntries = () => detailBatchState().entries;
|
|
384
|
-
const batchCounts = () => detailBatchState().counts;
|
|
385
|
-
const batchCount = (type) => {
|
|
386
|
-
const raw = batchCounts()[type];
|
|
387
|
-
return Number.isFinite(Number(raw)) ? Number(raw) : 0;
|
|
388
|
-
};
|
|
389
|
-
const otherBatchCount = () => Object.entries(batchCounts()).reduce((sum, pair) => {
|
|
390
|
-
return DETAIL_BATCH_OTHER_EXCLUDED_TYPES.includes(pair[0]) ? sum : sum + Number(pair[1] || 0);
|
|
391
|
-
}, 0);
|
|
392
|
-
const resolveDetailBatchQuery = (tab) => DETAIL_BATCH_SCOPE_BY_TAB[tab] || null;
|
|
393
|
-
const resolveDetailBatchCount = (tab) => {
|
|
394
|
-
if (tab === 'other') return otherBatchCount();
|
|
395
|
-
const type = DETAIL_BATCH_COUNT_TYPES[tab];
|
|
396
|
-
return typeof type === 'string' ? batchCount(type) : 0;
|
|
397
|
-
};
|
|
398
|
-
const hasRequestTrace = () => Boolean(state.detail && state.detail.type === 'request');
|
|
399
|
-
|
|
400
|
-
const prettyJson = (value) => {
|
|
401
|
-
try {
|
|
402
|
-
return JSON.stringify(value ?? {}, null, 2) ?? '{}';
|
|
403
|
-
} catch {
|
|
404
|
-
return String(value ?? '');
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
const registerCopyPayload = (text) => {
|
|
409
|
-
const id = 'copy-' + (++copySequence);
|
|
410
|
-
copyPayloads.set(id, text);
|
|
411
|
-
return id;
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
const renderCodeCard = (label, rawText, highlightedHtml, languageClass, options = {}) => {
|
|
415
|
-
const copyId = registerCopyPayload(rawText);
|
|
416
|
-
const wrapClass = options.wrap === true ? ' wrap' : '';
|
|
417
|
-
return [
|
|
418
|
-
'<section class="code-card">',
|
|
419
|
-
'<div class="code-toolbar">',
|
|
420
|
-
'<span class="code-label">' + escapeHtml(label) + '</span>',
|
|
421
|
-
'<button type="button" class="copy-button" data-action="copy-code" data-copy-id="' + escapeHtml(copyId) + '" title="Copy ' + escapeHtml(label) + '">',
|
|
422
|
-
COPY_ICON,
|
|
423
|
-
'</button>',
|
|
424
|
-
'</div>',
|
|
425
|
-
'<pre class="code-block ' + escapeHtml(languageClass) + wrapClass + '"><code>' + highlightedHtml + '</code></pre>',
|
|
426
|
-
'</section>'
|
|
427
|
-
].join('');
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
const renderTextCard = (label, value) => {
|
|
431
|
-
const source = String(value ?? '');
|
|
432
|
-
return renderCodeCard(label, source, escapeHtml(source), 'language-text', { wrap: true });
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
const renderHtmlPreview = (label, html, options = {}) => {
|
|
436
|
-
const source = String(html ?? '');
|
|
437
|
-
const copyId = registerCopyPayload(source);
|
|
438
|
-
const rawHtmlBlock = '<pre class="code-block language-html wrap"><code>' + escapeHtml(source) + '</code></pre>';
|
|
439
|
-
const rawHtmlSection = options.collapseSource === true
|
|
440
|
-
? '<details class="inline-collapse"><summary>View raw HTML source</summary>' + rawHtmlBlock + '</details>'
|
|
441
|
-
: rawHtmlBlock;
|
|
442
|
-
return [
|
|
443
|
-
'<section class="code-card">',
|
|
444
|
-
'<div class="code-toolbar">',
|
|
445
|
-
'<span class="code-label">' + escapeHtml(label) + '</span>',
|
|
446
|
-
'<button type="button" class="copy-button" data-action="copy-code" data-copy-id="' + escapeHtml(copyId) + '" title="Copy ' + escapeHtml(label) + '">',
|
|
447
|
-
COPY_ICON,
|
|
448
|
-
'</button>',
|
|
449
|
-
'</div>',
|
|
450
|
-
'<div class="html-preview-wrap"><iframe class="html-preview" sandbox="allow-same-origin" srcdoc="' + escapeHtml(source) + '"></iframe></div>',
|
|
451
|
-
rawHtmlSection,
|
|
452
|
-
'</section>'
|
|
453
|
-
].join('');
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
const highlightJson = (value, label = 'JSON') => {
|
|
457
|
-
const source = prettyJson(value);
|
|
458
|
-
let output = '';
|
|
459
|
-
let lastIndex = 0;
|
|
460
|
-
|
|
461
|
-
for (const match of source.matchAll(JSON_HIGHLIGHT_PATTERN)) {
|
|
462
|
-
const index = match.index ?? 0;
|
|
463
|
-
output += escapeHtml(source.slice(lastIndex, index));
|
|
464
|
-
const token = match[0];
|
|
465
|
-
if (match[1]) output += '<span class="tok-key">' + escapeHtml(match[1]) + '</span>';
|
|
466
|
-
else if (match[2]) output += '<span class="tok-punctuation">' + escapeHtml(match[2]) + '</span>';
|
|
467
|
-
else if (token === 'true' || token === 'false') output += '<span class="tok-boolean">' + token + '</span>';
|
|
468
|
-
else if (token === 'null') output += '<span class="tok-null">null</span>';
|
|
469
|
-
else if (/^"/.test(token)) output += '<span class="tok-string">' + escapeHtml(token) + '</span>';
|
|
470
|
-
else output += '<span class="tok-number">' + escapeHtml(token) + '</span>';
|
|
471
|
-
lastIndex = index + token.length;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
output += escapeHtml(source.slice(lastIndex));
|
|
475
|
-
return renderCodeCard(label, source, output, 'language-json', { wrap: true });
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
const highlightSql = (sql) => {
|
|
479
|
-
const source = String(sql || '');
|
|
480
|
-
let output = '';
|
|
481
|
-
let lastIndex = 0;
|
|
482
|
-
|
|
483
|
-
for (const match of source.matchAll(SQL_HIGHLIGHT_PATTERN)) {
|
|
484
|
-
const index = match.index ?? 0;
|
|
485
|
-
const token = match[0];
|
|
486
|
-
output += escapeHtml(source.slice(lastIndex, index));
|
|
487
|
-
|
|
488
|
-
if (token.startsWith('/*') || token.startsWith('--')) output += '<span class="tok-sql-comment">' + escapeHtml(token) + '</span>';
|
|
489
|
-
else if (token.startsWith("'")) output += '<span class="tok-sql-string">' + escapeHtml(token) + '</span>';
|
|
490
|
-
else if (token.charCodeAt(0) === 96) output += '<span class="tok-sql-identifier">' + escapeHtml(token) + '</span>';
|
|
491
|
-
else {
|
|
492
|
-
const numericIndex = token.startsWith('-') ? 1 : 0;
|
|
493
|
-
const numericChar = token.charAt(numericIndex);
|
|
494
|
-
if (numericChar >= '0' && numericChar <= '9') output += '<span class="tok-sql-number">' + escapeHtml(token) + '</span>';
|
|
495
|
-
else output += '<span class="tok-sql-keyword">' + escapeHtml(token) + '</span>';
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
lastIndex = index + token.length;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
output += escapeHtml(source.slice(lastIndex));
|
|
502
|
-
return renderCodeCard('SQL', source, output, 'language-sql');
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
const detailJson = (value, label = 'JSON') => highlightJson(value ?? {}, label);
|
|
506
|
-
|
|
507
|
-
const renderPayload = (label, value) => {
|
|
508
|
-
if (value === undefined) return '<p class="trace-note">No ' + escapeHtml(label.toLowerCase()) + ' was captured.</p>';
|
|
509
|
-
if (typeof value === 'string') {
|
|
510
|
-
return looksLikeHtml(value)
|
|
511
|
-
? renderHtmlPreview(label, value, { collapseSource: true })
|
|
512
|
-
: renderTextCard(label, value);
|
|
513
|
-
}
|
|
514
|
-
return detailJson(value, label);
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
const entrySummaryText = (entry) => {
|
|
518
|
-
const content = entry && entry.content ? entry.content : {};
|
|
519
|
-
if (entry.type === 'request') return [content.responseStatus || '', content.uri || ''].filter(Boolean).join(' ');
|
|
520
|
-
if (entry.type === 'query') return String(content.sql || '').slice(0, 160);
|
|
521
|
-
if (entry.type === 'exception') return [content.class || '', content.message || ''].filter(Boolean).join(': ');
|
|
522
|
-
if (entry.type === 'log') return '[' + String(content.level || 'log') + '] ' + String(content.message || '').slice(0, 160);
|
|
523
|
-
if (entry.type === 'job') return [content.name || '', content.status || 'queued'].filter(Boolean).join(' · ');
|
|
524
|
-
if (entry.type === 'cache') return [content.operation || '', content.key || '', content.payloadLogged ? '' : '(payload off)'].filter(Boolean).join(' ');
|
|
525
|
-
if (entry.type === 'schedule') return [content.name || '', content.status || 'ran'].filter(Boolean).join(' · ');
|
|
526
|
-
if (entry.type === 'mail') return ['To ' + (content.to || 'unknown'), content.subject || 'No subject'].join(' · ');
|
|
527
|
-
if (entry.type === 'auth') return [content.event || 'auth', content.userId ? '#' + content.userId : ''].filter(Boolean).join(' ');
|
|
528
|
-
if (entry.type === 'event') return String(content.name || 'event');
|
|
529
|
-
if (entry.type === 'model') return [content.action || '', content.model || ''].filter(Boolean).join(' ');
|
|
530
|
-
if (entry.type === 'notification') return [content.notification || '', content.message || (content.channels || []).join(', ')].filter(Boolean).join(' -> ');
|
|
531
|
-
if (entry.type === 'redis') return String(content.command || 'redis');
|
|
532
|
-
if (entry.type === 'gate') return [content.ability || '', content.result || ''].filter(Boolean).join(' · ');
|
|
533
|
-
if (entry.type === 'middleware') return [content.name || '', content.event || ''].filter(Boolean).join(' · ');
|
|
534
|
-
if (entry.type === 'command') return [content.name || '', content.exitCode !== undefined ? 'exit=' + content.exitCode : ''].filter(Boolean).join(' ');
|
|
535
|
-
if (entry.type === 'batch') return [content.name || '', 'processed ' + (content.processed || 0) + '/' + (content.total || 0)].join(' · ');
|
|
536
|
-
if (entry.type === 'view') return String(content.template || 'view');
|
|
537
|
-
if (entry.type === 'client_request') return [content.method || '', content.url || '', content.responseStatus ? '[' + content.responseStatus + ']' : content.error ? '[failed]' : ''].filter(Boolean).join(' ');
|
|
538
|
-
return JSON.stringify(content).slice(0, 160);
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
const entrySummaryHtml = (entry) => {
|
|
542
|
-
const content = entry && entry.content ? entry.content : {};
|
|
543
|
-
if (entry.type === 'request') {
|
|
544
|
-
return '<div class="summary">' + statusBadgeHtml(content.responseStatus) + ' <span class="mono">' + escapeHtml(content.uri || '') + '</span></div><div class="summary-sub">Incoming request</div>';
|
|
545
|
-
}
|
|
546
|
-
if (entry.type === 'client_request') {
|
|
547
|
-
const clientParts = [
|
|
548
|
-
content.method ? '<span class="mono">' + escapeHtml(content.method) + '</span>' : '',
|
|
549
|
-
content.url ? '<span class="mono">' + escapeHtml(content.url) + '</span>' : '',
|
|
550
|
-
statusBadgeHtml(content.responseStatus) || (content.error ? '<span class="status-pill status-5xx">Failed</span>' : '')
|
|
551
|
-
].filter(Boolean).join(' ');
|
|
552
|
-
return '<div class="summary">' + clientParts + '</div><div class="summary-sub">Outbound HTTP call</div>';
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
const summary = escapeHtml(entrySummaryText(entry) || 'No summary available');
|
|
556
|
-
const secondary = [
|
|
557
|
-
entry.type === 'request' ? 'Incoming request' : '',
|
|
558
|
-
entry.type === 'query' ? 'Database query' : '',
|
|
559
|
-
entry.type === 'exception' ? 'Unhandled error' : '',
|
|
560
|
-
entry.type === 'client_request' ? 'Outbound HTTP call' : ''
|
|
561
|
-
].find(Boolean) || 'Trace record';
|
|
562
|
-
return '<div class="summary">' + summary + '</div><div class="summary-sub">' + escapeHtml(secondary) + '</div>';
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
const entrySummaryInlineHtml = (entry) => {
|
|
566
|
-
const summary = escapeHtml(entrySummaryText(entry) || 'No summary available');
|
|
567
|
-
const secondary = [
|
|
568
|
-
entry.type === 'request' ? 'Incoming request' : '',
|
|
569
|
-
entry.type === 'query' ? 'Database query' : '',
|
|
570
|
-
entry.type === 'exception' ? 'Unhandled error' : '',
|
|
571
|
-
entry.type === 'client_request' ? 'Outbound HTTP call' : ''
|
|
572
|
-
].find(Boolean) || 'Trace record';
|
|
573
|
-
return '<span class="summary">' + summary + '</span><span class="summary-sub">' + escapeHtml(secondary) + '</span>';
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
const renderMetricBox = (title, items) => {
|
|
577
|
-
return [
|
|
578
|
-
'<section class="detail-box">',
|
|
579
|
-
'<h4>' + escapeHtml(title) + '</h4>',
|
|
580
|
-
'<dl>',
|
|
581
|
-
items.map((item) => '<dt>' + escapeHtml(item.label) + '</dt><dd>' + item.value + '</dd>').join(''),
|
|
582
|
-
'</dl>',
|
|
583
|
-
'</section>'
|
|
584
|
-
].join('');
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
const renderEntryBody = (entry) => {
|
|
588
|
-
const content = entry && entry.content ? entry.content : {};
|
|
589
|
-
|
|
590
|
-
if (entry.type === 'query') {
|
|
591
|
-
return [
|
|
592
|
-
'<div class="detail-grid">',
|
|
593
|
-
renderMetricBox('Query', [
|
|
594
|
-
{ label: 'Connection', value: escapeHtml(content.connection || 'default') },
|
|
595
|
-
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) },
|
|
596
|
-
{ label: 'Slow', value: escapeHtml(content.slow ? 'Yes' : 'No') },
|
|
597
|
-
{ label: 'Bindings', value: escapeHtml(content.bindingsIncluded === false ? 'Hidden' : 'Included') },
|
|
598
|
-
{ label: 'Hash', value: '<span class="mono">' + escapeHtml(content.hash || '') + '</span>' }
|
|
599
|
-
]),
|
|
600
|
-
renderMetricBox('Runtime', [
|
|
601
|
-
{ label: 'Hostname', value: escapeHtml(content.hostname || '') },
|
|
602
|
-
{ label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' }
|
|
603
|
-
]),
|
|
604
|
-
'</div>',
|
|
605
|
-
highlightSql(content.sql || ''),
|
|
606
|
-
content.bindingsIncluded === false ? '<p class="trace-note">SQL bindings were hidden for this entry.</p>' : (Array.isArray(content.bindings) ? detailJson(content.bindings, 'Bindings Json') : '')
|
|
607
|
-
].join('');
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if (entry.type === 'log') {
|
|
611
|
-
return [
|
|
612
|
-
'<div class="detail-grid">',
|
|
613
|
-
renderMetricBox('Log', [
|
|
614
|
-
{ label: 'Level', value: escapeHtml(content.level || '') },
|
|
615
|
-
{ label: 'Hostname', value: escapeHtml(content.hostname || '') }
|
|
616
|
-
]),
|
|
617
|
-
renderMetricBox('Message', [
|
|
618
|
-
{ label: 'Text', value: escapeHtml(content.message || '') }
|
|
619
|
-
]),
|
|
620
|
-
'</div>',
|
|
621
|
-
content.context ? detailJson(content.context) : '<p class="trace-note">No log context was captured for this entry.</p>'
|
|
622
|
-
].join('');
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (entry.type === 'exception') {
|
|
626
|
-
return [
|
|
627
|
-
'<div class="detail-grid">',
|
|
628
|
-
renderMetricBox('Exception', [
|
|
629
|
-
{ label: 'Class', value: escapeHtml(content.class || '') },
|
|
630
|
-
{ label: 'Message', value: escapeHtml(content.message || '') },
|
|
631
|
-
{ label: 'File', value: '<span class="mono">' + escapeHtml(content.file || '') + '</span>' },
|
|
632
|
-
{ label: 'Line', value: escapeHtml(content.line || '') }
|
|
633
|
-
]),
|
|
634
|
-
renderMetricBox('Runtime', [
|
|
635
|
-
{ label: 'Hostname', value: escapeHtml(content.hostname || '') },
|
|
636
|
-
{ label: 'User', value: escapeHtml(content.userId || 'Anonymous') },
|
|
637
|
-
{ label: 'Occurrences', value: escapeHtml(content.occurrences || 1) }
|
|
638
|
-
]),
|
|
639
|
-
'</div>',
|
|
640
|
-
detailJson({ trace: content.trace || [], linePreview: content.linePreview || {} })
|
|
641
|
-
].join('');
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
if (entry.type === 'client_request') {
|
|
645
|
-
return [
|
|
646
|
-
'<div class="detail-grid">',
|
|
647
|
-
renderMetricBox('Request', [
|
|
648
|
-
{ label: 'Method', value: escapeHtml(content.method || '') },
|
|
649
|
-
{ label: 'URL', value: '<span class="mono">' + escapeHtml(content.url || '') + '</span>' },
|
|
650
|
-
{ label: 'Status', value: statusBadgeHtml(content.responseStatus) || escapeHtml(content.error ? 'Failed' : 'Pending') },
|
|
651
|
-
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
|
|
652
|
-
]),
|
|
653
|
-
renderMetricBox('Runtime', [
|
|
654
|
-
{ label: 'Hostname', value: escapeHtml(content.hostname || '') },
|
|
655
|
-
{ label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' },
|
|
656
|
-
{ label: 'Error', value: escapeHtml(content.error || '-') }
|
|
657
|
-
]),
|
|
658
|
-
'</div>',
|
|
659
|
-
'<div class="detail-stack">',
|
|
660
|
-
detailJson(content.requestHeaders || {}, 'Request Header Json'),
|
|
661
|
-
renderPayload('Request Body', content.requestBody),
|
|
662
|
-
detailJson(content.responseHeaders || {}, 'Response Header Json'),
|
|
663
|
-
renderPayload('Response Body', content.responseBody),
|
|
664
|
-
'</div>'
|
|
665
|
-
].join('');
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
if (entry.type === 'cache') {
|
|
669
|
-
return [
|
|
670
|
-
'<div class="detail-grid">',
|
|
671
|
-
renderMetricBox('Cache', [
|
|
672
|
-
{ label: 'Operation', value: escapeHtml(content.operation || '') },
|
|
673
|
-
{ label: 'Key', value: '<span class="mono">' + escapeHtml(content.key || '') + '</span>' },
|
|
674
|
-
{ label: 'Store', value: escapeHtml(content.store || 'default') },
|
|
675
|
-
{ label: 'Hit', value: escapeHtml(content.hit === undefined ? '-' : (content.hit ? 'Yes' : 'No')) },
|
|
676
|
-
{ label: 'Payload', value: escapeHtml(content.payloadLogged ? 'Captured' : 'Disabled') },
|
|
677
|
-
{ label: 'TTL', value: escapeHtml(content.ttl === undefined ? '-' : String(content.ttl)) },
|
|
678
|
-
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
|
|
679
|
-
]),
|
|
680
|
-
renderMetricBox('Runtime', [
|
|
681
|
-
{ label: 'Hostname', value: escapeHtml(content.hostname || '') },
|
|
682
|
-
{ label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' }
|
|
683
|
-
]),
|
|
684
|
-
'</div>',
|
|
685
|
-
content.payloadLogged ? renderPayload('Cache Payload', content.payload) : '<p class="trace-note">Cache payload logging is disabled. Set TRACE_CACHE_PAYLOADS=true to include values.</p>'
|
|
686
|
-
].join('');
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
if (entry.type === 'mail') {
|
|
690
|
-
return [
|
|
691
|
-
'<div class="detail-grid">',
|
|
692
|
-
renderMetricBox('Mail', [
|
|
693
|
-
{ label: 'To', value: escapeHtml(content.to || '') },
|
|
694
|
-
{ label: 'Subject', value: escapeHtml(content.subject || '') },
|
|
695
|
-
{ label: 'Template', value: escapeHtml(content.template || '-') },
|
|
696
|
-
{ label: 'Hostname', value: escapeHtml(content.hostname || '') }
|
|
697
|
-
]),
|
|
698
|
-
'</div>',
|
|
699
|
-
'<div class="detail-stack">',
|
|
700
|
-
renderPayload('Mail Html', content.html),
|
|
701
|
-
renderPayload('Mail Text', content.text),
|
|
702
|
-
'</div>'
|
|
703
|
-
].join('');
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
if (entry.type === 'notification') {
|
|
707
|
-
return [
|
|
708
|
-
'<div class="detail-grid">',
|
|
709
|
-
renderMetricBox('Notification', [
|
|
710
|
-
{ label: 'Notification', value: escapeHtml(content.notification || '') },
|
|
711
|
-
{ label: 'Channels', value: escapeHtml((content.channels || []).join(', ') || '-') },
|
|
712
|
-
{ label: 'Recipient', value: escapeHtml(content.notifiable || '-') },
|
|
713
|
-
{ label: 'Hostname', value: escapeHtml(content.hostname || '') }
|
|
714
|
-
]),
|
|
715
|
-
'</div>',
|
|
716
|
-
'<div class="detail-stack">',
|
|
717
|
-
renderPayload('Message', content.message),
|
|
718
|
-
content.payload === undefined ? '<p class="trace-note">No additional notification payload was captured.</p>' : detailJson(content.payload, 'Notification Payload Json'),
|
|
719
|
-
'</div>'
|
|
720
|
-
].join('');
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
return detailJson(content);
|
|
724
|
-
};
|
|
725
|
-
|
|
726
|
-
const renderTraceItems = (entries, options = {}) => {
|
|
727
|
-
if (entries.length === 0) return '<p class="trace-note">No related entries captured.</p>';
|
|
728
|
-
|
|
729
|
-
const collapsible = options.collapsible !== false;
|
|
730
|
-
const isInitiallyOpen = options.collapsed === false;
|
|
731
|
-
|
|
732
|
-
return '<div class="trace-panel">' + entries.map((entry) => {
|
|
733
|
-
if (collapsible) {
|
|
734
|
-
return [
|
|
735
|
-
'<details class="trace-item trace-disclosure"' + (isInitiallyOpen ? ' open' : '') + '>',
|
|
736
|
-
'<summary class="trace-item-head trace-summary">',
|
|
737
|
-
'<span class="trace-summary-icon">' + DISCLOSURE_ICON + '</span>',
|
|
738
|
-
'<span class="trace-summary-main">',
|
|
739
|
-
'<span><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></span>',
|
|
740
|
-
'<span class="trace-summary-copy">' + entrySummaryInlineHtml(entry) + '</span>',
|
|
741
|
-
'</span>',
|
|
742
|
-
'<span class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></span>',
|
|
743
|
-
'</summary>',
|
|
744
|
-
'<div class="trace-disclosure-body">',
|
|
745
|
-
'<div>' + tagsHtml(entry.tags) + '</div>',
|
|
746
|
-
renderEntryBody(entry),
|
|
747
|
-
'</div>',
|
|
748
|
-
'</details>'
|
|
749
|
-
].join('');
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
return [
|
|
753
|
-
'<section class="trace-item">',
|
|
754
|
-
'<div class="trace-item-head">',
|
|
755
|
-
'<div>',
|
|
756
|
-
'<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span>',
|
|
757
|
-
'</div>',
|
|
758
|
-
'<div class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div>',
|
|
759
|
-
'</div>',
|
|
760
|
-
'<div class="trace-item-summary">',
|
|
761
|
-
entrySummaryHtml(entry),
|
|
762
|
-
'<div>' + tagsHtml(entry.tags) + '</div>',
|
|
763
|
-
renderEntryBody(entry),
|
|
764
|
-
'</div>',
|
|
765
|
-
'</section>'
|
|
766
|
-
].join('');
|
|
767
|
-
}).join('') + '</div>';
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
const renderDetailBatchPanel = (tab) => {
|
|
771
|
-
const detailBatch = detailBatchState();
|
|
772
|
-
const count = resolveDetailBatchCount(tab);
|
|
773
|
-
if (detailBatch.loading && detailBatch.scope === tab) {
|
|
774
|
-
return '<p class="trace-note">Loading related entries...</p>';
|
|
775
|
-
}
|
|
776
|
-
if (count === 0) {
|
|
777
|
-
return '<p class="trace-note">No related entries captured.</p>';
|
|
778
|
-
}
|
|
779
|
-
if (detailBatch.scope !== tab) {
|
|
780
|
-
return '<p class="trace-note">Open this tab to load the first page of related entries.</p>';
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const totalPages = Math.max(1, Math.ceil(detailBatch.total / Math.max(1, detailBatch.perPage)));
|
|
784
|
-
|
|
785
|
-
return [
|
|
786
|
-
renderTraceItems(batchEntries()),
|
|
787
|
-
'<div class="pagination"><span>Page ' + escapeHtml(detailBatch.page) + ' of ' + escapeHtml(totalPages) + ' · ' + escapeHtml(detailBatch.total) + ' related entries</span><div class="pagination-controls"><button type="button" data-action="detail-batch-prev"' + (detailBatch.page <= 1 ? ' disabled' : '') + '>Previous</button><button type="button" data-action="detail-batch-next"' + (detailBatch.page >= totalPages ? ' disabled' : '') + '>Next</button></div></div>'
|
|
788
|
-
].join('');
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
const renderRequestTrace = (main) => {
|
|
792
|
-
const entry = state.detail;
|
|
793
|
-
const content = entry && entry.content ? entry.content : {};
|
|
794
|
-
const traceTabs = [
|
|
795
|
-
{ id: 'summary', label: 'Summary' },
|
|
796
|
-
{ id: 'payload', label: 'Payload' },
|
|
797
|
-
{ id: 'headers', label: 'Headers' },
|
|
798
|
-
{ id: 'response', label: 'Response' },
|
|
799
|
-
{ id: 'queries', label: 'Queries', count: resolveDetailBatchCount('queries') },
|
|
800
|
-
{ id: 'middleware', label: 'Middleware', count: resolveDetailBatchCount('middleware') },
|
|
801
|
-
{ id: 'models', label: 'Models', count: resolveDetailBatchCount('models') },
|
|
802
|
-
{ id: 'logs', label: 'Logs', count: resolveDetailBatchCount('logs') },
|
|
803
|
-
{ id: 'exceptions', label: 'Exceptions', count: resolveDetailBatchCount('exceptions') },
|
|
804
|
-
{ id: 'http', label: 'HTTP', count: resolveDetailBatchCount('http') },
|
|
805
|
-
{ id: 'cache', label: 'Cache', count: resolveDetailBatchCount('cache') },
|
|
806
|
-
{ id: 'other', label: 'Other', count: resolveDetailBatchCount('other') }
|
|
807
|
-
];
|
|
808
|
-
const currentTab = traceTabs.some((tab) => tab.id === state.detailTab) ? state.detailTab : 'summary';
|
|
809
|
-
const panels = {
|
|
810
|
-
summary: [
|
|
811
|
-
'<div class="detail-grid">',
|
|
812
|
-
renderMetricBox('Request', [
|
|
813
|
-
{ label: 'Method', value: escapeHtml(content.method || '') },
|
|
814
|
-
{ label: 'Path', value: '<span class="mono">' + escapeHtml(content.uri || '') + '</span>' },
|
|
815
|
-
{ label: 'Status', value: escapeHtml(content.responseStatus || '') },
|
|
816
|
-
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
|
|
817
|
-
]),
|
|
818
|
-
renderMetricBox('Runtime', [
|
|
819
|
-
{ label: 'Hostname', value: escapeHtml(content.hostname || '') },
|
|
820
|
-
{ label: 'User', value: escapeHtml(content.userId || 'Anonymous') },
|
|
821
|
-
{ label: 'Memory', value: escapeHtml(content.memory === null || content.memory === undefined ? 'Unavailable' : String(content.memory)) },
|
|
822
|
-
{ label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '') + '</span>' }
|
|
823
|
-
]),
|
|
824
|
-
renderMetricBox('Tags', [
|
|
825
|
-
{ label: 'Values', value: tagsHtml(entry.tags) || '<span class="activity-time">-</span>' }
|
|
826
|
-
]),
|
|
827
|
-
renderMetricBox('Route middleware', [
|
|
828
|
-
{ label: 'Attached', value: escapeHtml(Array.isArray(content.middleware) && content.middleware.length > 0 ? content.middleware.join(', ') : 'None') }
|
|
829
|
-
]),
|
|
830
|
-
'</div>'
|
|
831
|
-
].join(''),
|
|
832
|
-
payload: detailJson(content.payload || {}, 'Payload Json'),
|
|
833
|
-
headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
834
|
-
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: statusBadgeHtml(content.responseStatus) || escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
835
|
-
queries: renderDetailBatchPanel('queries'),
|
|
836
|
-
middleware: renderDetailBatchPanel('middleware'),
|
|
837
|
-
models: renderDetailBatchPanel('models'),
|
|
838
|
-
logs: renderDetailBatchPanel('logs'),
|
|
839
|
-
exceptions: renderDetailBatchPanel('exceptions'),
|
|
840
|
-
http: renderDetailBatchPanel('http'),
|
|
841
|
-
cache: renderDetailBatchPanel('cache'),
|
|
842
|
-
other: renderDetailBatchPanel('other')
|
|
843
|
-
};
|
|
844
|
-
|
|
845
|
-
main.innerHTML = [
|
|
846
|
-
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
847
|
-
'<section class="panel detail-card">',
|
|
848
|
-
'<div>' + (entry.type === 'request'
|
|
849
|
-
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + statusBadgeHtml(content.responseStatus) + ' <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
|
|
850
|
-
: '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
|
|
851
|
-
'<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
|
|
852
|
-
'<div class="trace-tabs">',
|
|
853
|
-
traceTabs.map((tab) => '<button type="button" class="trace-tab' + (tab.id === currentTab ? ' active' : '') + '" data-action="detail-tab" data-tab="' + escapeHtml(tab.id) + '">' + escapeHtml(tab.label) + (tab.count !== undefined ? ' (' + escapeHtml(tab.count) + ')' : '') + '</button>').join(''),
|
|
854
|
-
'</div>',
|
|
855
|
-
panels[currentTab] || panels.summary,
|
|
856
|
-
'</section>'
|
|
857
|
-
].join('');
|
|
858
|
-
};
|
|
859
|
-
|
|
860
|
-
const statsCardsHtml = (stats) => {
|
|
861
|
-
const total = Object.values(stats).reduce((sum, value) => sum + Number(value || 0), 0);
|
|
862
|
-
const cards = [{ label: 'Total entries', value: total, meta: 'Stored trace entries.' }]
|
|
863
|
-
.concat(Object.entries(stats).filter((pair) => Number(pair[1]) > 0).map((pair) => ({ label: pair[0], value: Number(pair[1]), meta: pair[0] === 'query' ? 'Captured queries.' : 'Captured ' + pair[0] + '.' })));
|
|
864
|
-
return '<div class="stats-grid">' + cards.map((card) => '<section class="panel stat-card"><div class="stat-label">' + escapeHtml(card.label) + '</div><div class="stat-value">' + escapeHtml(card.value) + '</div><div class="stat-meta">' + escapeHtml(card.meta) + '</div></section>').join('') + '</div>';
|
|
865
|
-
};
|
|
866
|
-
|
|
867
|
-
const renderOverview = async (main) => {
|
|
868
|
-
main.innerHTML = '<div class="panel empty">Loading trace overview...</div>';
|
|
869
|
-
try {
|
|
870
|
-
const results = await Promise.all([api('/stats'), api('/entries?perPage=8&page=1')]);
|
|
871
|
-
const stats = results[0].stats;
|
|
872
|
-
const recent = results[1];
|
|
873
|
-
const recentRows = recent.data || [];
|
|
874
|
-
const recentTable = recentRows.length === 0
|
|
875
|
-
? '<div class="empty">No trace entries recorded.</div>'
|
|
876
|
-
: '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Summary</th><th>Tags</th><th>Duration</th><th>Happened</th></tr></thead><tbody>' + recentRows.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td data-label="Type"><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></td><td data-label="Summary">' + entrySummaryHtml(entry) + '</td><td data-label="Tags">' + tagsHtml(entry.tags) + '</td><td data-label="Duration">' + durationHtml(entry) + '</td><td data-label="Happened" class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('') + '</tbody></table></div>';
|
|
877
|
-
const activityList = recentRows.length === 0
|
|
878
|
-
? '<div class="empty">No recent activity.</div>'
|
|
879
|
-
: '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span>' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div><div class="activity-summary">' + escapeHtml(entrySummaryText(entry)) + '</div></li>').join('') + '</ul>';
|
|
880
|
-
|
|
881
|
-
main.innerHTML = [
|
|
882
|
-
statsCardsHtml(stats),
|
|
883
|
-
'<div class="content-grid">',
|
|
884
|
-
'<section class="panel">',
|
|
885
|
-
'<div class="section-head"><div><h3>Recent entries</h3><p>Latest captured records.</p></div><button type="button" class="btn btn-primary" data-action="go-page" data-page="entries">Open entries</button></div>',
|
|
886
|
-
recentTable,
|
|
887
|
-
'</section>',
|
|
888
|
-
'<div class="side-stack">',
|
|
889
|
-
'<section class="panel">',
|
|
890
|
-
'<div class="section-head"><div><h3>Actions</h3><p>Trace maintenance.</p></div></div>',
|
|
891
|
-
'<div class="toolbar"><button type="button" class="btn btn-danger" data-action="clear-all">Clear entries</button><button type="button" class="btn btn-ghost" data-action="go-page" data-page="monitoring">Manage tags</button></div>',
|
|
892
|
-
'</section>',
|
|
893
|
-
'<section class="panel">',
|
|
894
|
-
'<div class="section-head"><div><h3>Recent activity</h3><p>Latest captured events.</p></div></div>',
|
|
895
|
-
activityList,
|
|
896
|
-
'</section>',
|
|
897
|
-
'</div>',
|
|
898
|
-
'</div>'
|
|
899
|
-
].join('');
|
|
900
|
-
} catch (error) {
|
|
901
|
-
main.innerHTML = '<div class="panel empty">Error loading overview: ' + escapeHtml(error.message) + '</div>';
|
|
902
|
-
}
|
|
903
|
-
};
|
|
904
|
-
|
|
905
|
-
const renderEntries = async (main) => {
|
|
906
|
-
if (state.detail) {
|
|
907
|
-
renderDetail(main);
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
main.innerHTML = '<div class="panel empty">Loading entries...</div>';
|
|
912
|
-
try {
|
|
913
|
-
const qs = new URLSearchParams({ page: String(state.entriesPage), perPage: '50' });
|
|
914
|
-
if (state.entriesFilter.type) qs.set('type', state.entriesFilter.type);
|
|
915
|
-
if (state.entriesFilter.tag) qs.set('tag', state.entriesFilter.tag);
|
|
916
|
-
if (state.entriesFilter.batchId) qs.set('batchId', state.entriesFilter.batchId);
|
|
917
|
-
|
|
918
|
-
const response = await api('/entries?' + qs.toString());
|
|
919
|
-
const data = response.data || [];
|
|
920
|
-
const total = Number(response.total || 0);
|
|
921
|
-
const perPage = Number(response.perPage || 50);
|
|
922
|
-
const totalPages = Math.max(1, Math.ceil(total / perPage));
|
|
923
|
-
const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td data-label="Type"><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></td><td data-label="Summary">' + entrySummaryHtml(entry) + '</td><td data-label="Tags">' + tagsHtml(entry.tags) + '</td><td data-label="Duration">' + durationHtml(entry) + '</td><td data-label="Batch" class="mono">' + batchSnippet(entry.batchId) + '</td><td data-label="Happened" class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('');
|
|
924
|
-
|
|
925
|
-
main.innerHTML = [
|
|
926
|
-
'<section class="panel">',
|
|
927
|
-
'<div class="section-head"><div><h3>Entries</h3><p>Filter by type, tag, or batch.</p></div></div>',
|
|
928
|
-
'<div class="toolbar">',
|
|
929
|
-
'<select id="f-type"><option value="">All types</option>' + ENTRY_TYPES.map((type) => '<option value="' + escapeHtml(type) + '"' + (state.entriesFilter.type === type ? ' selected' : '') + '>' + escapeHtml(type) + '</option>').join('') + '</select>',
|
|
930
|
-
'<input id="f-tag" type="text" placeholder="Tag" value="' + escapeHtml(state.entriesFilter.tag) + '">',
|
|
931
|
-
'<input id="f-batch" type="text" placeholder="Batch ID" value="' + escapeHtml(state.entriesFilter.batchId) + '">',
|
|
932
|
-
'<button type="button" class="btn btn-ghost" data-action="clear-filters">Reset</button>',
|
|
933
|
-
'</div>',
|
|
934
|
-
data.length === 0 ? '<div class="empty">No entries match the current filter.</div>' : '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Summary</th><th>Tags</th><th>Duration</th><th>Batch</th><th>Happened</th></tr></thead><tbody>' + rows + '</tbody></table></div>',
|
|
935
|
-
'<div class="pagination"><span>Page ' + escapeHtml(state.entriesPage) + ' of ' + escapeHtml(totalPages) + ' · ' + escapeHtml(total) + ' total entries</span><div class="pagination-controls"><button type="button" data-action="page-prev"' + (state.entriesPage <= 1 ? ' disabled' : '') + '>Previous</button><button type="button" data-action="page-next"' + (state.entriesPage >= totalPages ? ' disabled' : '') + '>Next</button></div></div>',
|
|
936
|
-
'</section>'
|
|
937
|
-
].join('');
|
|
938
|
-
} catch (error) {
|
|
939
|
-
main.innerHTML = '<div class="panel empty">Error loading entries: ' + escapeHtml(error.message) + '</div>';
|
|
940
|
-
}
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
const renderDetail = (main) => {
|
|
944
|
-
if (!state.detail) {
|
|
945
|
-
state = { ...state, detail: null, detailBatch: null, detailTab: 'summary' };
|
|
946
|
-
renderEntries(main);
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
if (hasRequestTrace()) {
|
|
951
|
-
renderRequestTrace(main);
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
const entry = state.detail;
|
|
956
|
-
main.innerHTML = [
|
|
957
|
-
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
958
|
-
'<section class="panel detail-card">',
|
|
959
|
-
'<div><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
|
|
960
|
-
'<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
|
|
961
|
-
'<div class="detail-stack">',
|
|
962
|
-
renderEntryBody(entry),
|
|
963
|
-
'</div>',
|
|
964
|
-
'</section>'
|
|
965
|
-
].join('');
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
const renderMonitoring = async (main) => {
|
|
969
|
-
main.innerHTML = '<div class="panel empty">Loading monitoring tags...</div>';
|
|
970
|
-
try {
|
|
971
|
-
const result = await api('/monitoring');
|
|
972
|
-
const tags = result.tags || [];
|
|
973
|
-
main.innerHTML = [
|
|
974
|
-
'<section class="panel">',
|
|
975
|
-
'<div class="section-head"><div><h3>Monitoring tags</h3><p>Save the tags you filter by often so they stay one click away.</p></div></div>',
|
|
976
|
-
'<div class="monitoring-wrap">',
|
|
977
|
-
'<div class="tag-list">',
|
|
978
|
-
tags.length === 0 ? '<span class="helper-text">No tags monitored yet. Add tags like auth, checkout, queue:emails, or nightly-sync to pin them here.</span>' : tags.map((tag) => '<span class="tag-item"><a class="tag mono" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '" href="' + escapeHtml(buildEntriesHref({ tag })) + '">' + escapeHtml(tag) + '</a><button type="button" class="tag-remove" data-action="remove-tag" data-tag="' + escapeHtml(tag) + '">x</button></span>').join(''),
|
|
979
|
-
'</div>',
|
|
980
|
-
'<p class="helper-text">Click a saved tag to filter the entries list by that exact tag. Because each tag is a real link, you can also open it in a new tab. Removing a saved tag only removes the shortcut here; it does not delete any trace entries.</p>',
|
|
981
|
-
'<div class="toolbar" style="padding:0;margin-top:8px">',
|
|
982
|
-
'<input id="new-tag" class="control" type="text" placeholder="Add tag, for example checkout">',
|
|
983
|
-
'<button type="button" class="btn btn-primary" data-action="add-tag">Add tag</button>',
|
|
984
|
-
'</div>',
|
|
985
|
-
'</div>',
|
|
986
|
-
'</section>'
|
|
987
|
-
].join('');
|
|
988
|
-
} catch (error) {
|
|
989
|
-
main.innerHTML = '<div class="panel empty">Error loading monitoring tags: ' + escapeHtml(error.message) + '</div>';
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
|
-
|
|
993
|
-
const render = async () => {
|
|
994
|
-
const main = document.getElementById('main');
|
|
995
|
-
if (!main) return;
|
|
996
|
-
|
|
997
|
-
setPageCopy(state.page);
|
|
998
|
-
updateThemeButton();
|
|
999
|
-
const activeShortcut = activeEntryShortcut();
|
|
1000
|
-
document.querySelectorAll('[data-page]').forEach((button) => {
|
|
1001
|
-
button.classList.toggle('active', button.getAttribute('data-page') === state.page);
|
|
1002
|
-
});
|
|
1003
|
-
document.querySelectorAll('[data-action="type-shortcut"]').forEach((button) => {
|
|
1004
|
-
button.classList.toggle('active', button.getAttribute('data-type') === activeShortcut);
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
syncUrl();
|
|
1008
|
-
|
|
1009
|
-
if (state.page === 'overview') await renderOverview(main);
|
|
1010
|
-
if (state.page === 'entries') await renderEntries(main);
|
|
1011
|
-
if (state.page === 'monitoring') await renderMonitoring(main);
|
|
1012
|
-
};
|
|
1013
|
-
|
|
1014
|
-
const setPage = (page) => {
|
|
1015
|
-
state = { ...state, page, entriesPage: 1, detail: null, detailBatch: null, detailTab: 'summary' };
|
|
1016
|
-
render();
|
|
1017
|
-
};
|
|
1018
|
-
|
|
1019
|
-
const setTypeShortcut = (type) => {
|
|
1020
|
-
state = { ...state, page: 'entries', detail: null, detailBatch: null, detailTab: 'summary', entriesPage: 1, entriesFilter: { ...state.entriesFilter, type } };
|
|
1021
|
-
render();
|
|
1022
|
-
};
|
|
1023
|
-
|
|
1024
|
-
const filterByTag = (tag) => {
|
|
1025
|
-
state = { ...state, page: 'entries', detail: null, detailBatch: null, detailTab: 'summary', entriesPage: 1, entriesFilter: { ...state.entriesFilter, tag, batchId: '' } };
|
|
1026
|
-
render();
|
|
1027
|
-
};
|
|
1028
|
-
|
|
1029
|
-
const syncFilters = () => {
|
|
1030
|
-
const typeInput = document.getElementById('f-type');
|
|
1031
|
-
const tagInput = document.getElementById('f-tag');
|
|
1032
|
-
const batchInput = document.getElementById('f-batch');
|
|
1033
|
-
state = {
|
|
1034
|
-
...state,
|
|
1035
|
-
entriesPage: 1,
|
|
1036
|
-
entriesFilter: {
|
|
1037
|
-
type: typeInput && 'value' in typeInput ? String(typeInput.value || '') : '',
|
|
1038
|
-
tag: tagInput && 'value' in tagInput ? String(tagInput.value || '') : '',
|
|
1039
|
-
batchId: batchInput && 'value' in batchInput ? String(batchInput.value || '') : ''
|
|
1040
|
-
}
|
|
1041
|
-
};
|
|
1042
|
-
render();
|
|
1043
|
-
};
|
|
1044
|
-
|
|
1045
|
-
const clearAll = async () => {
|
|
1046
|
-
if (!window.confirm('Delete all trace entries?')) return;
|
|
1047
|
-
try {
|
|
1048
|
-
await api('/entries', { method: 'DELETE' });
|
|
1049
|
-
state = { ...state, detail: null, detailBatch: null, detailTab: 'summary', entriesPage: 1 };
|
|
1050
|
-
render();
|
|
1051
|
-
} catch (error) {
|
|
1052
|
-
window.alert(error.message);
|
|
1053
|
-
}
|
|
1054
|
-
};
|
|
1055
|
-
|
|
1056
|
-
const showDetail = async (uuid) => {
|
|
1057
|
-
try {
|
|
1058
|
-
const detailResult = await api('/entries/' + encodeURIComponent(uuid));
|
|
1059
|
-
const entry = detailResult.entry;
|
|
1060
|
-
let detailBatch = null;
|
|
1061
|
-
if (entry.type === 'request' && entry.batchId) {
|
|
1062
|
-
const batch = await api('/batch/' + encodeURIComponent(entry.batchId) + '?countsOnly=true');
|
|
1063
|
-
detailBatch = createDetailBatchState(batch);
|
|
1064
|
-
}
|
|
1065
|
-
state = { ...state, detail: entry, detailBatch, detailTab: 'summary', page: 'entries' };
|
|
1066
|
-
render();
|
|
1067
|
-
} catch (error) {
|
|
1068
|
-
window.alert(error.message);
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
1071
|
-
|
|
1072
|
-
const loadDetailBatchTab = async (tab, page = 1) => {
|
|
1073
|
-
const detail = state.detail;
|
|
1074
|
-
if (!detail || detail.type !== 'request' || !detail.batchId) return;
|
|
1075
|
-
|
|
1076
|
-
const query = resolveDetailBatchQuery(tab);
|
|
1077
|
-
if (!query) {
|
|
1078
|
-
state = { ...state, detailTab: tab };
|
|
1079
|
-
render();
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
const previous = detailBatchState();
|
|
1084
|
-
state = {
|
|
1085
|
-
...state,
|
|
1086
|
-
detailTab: tab,
|
|
1087
|
-
detailBatch: {
|
|
1088
|
-
...previous,
|
|
1089
|
-
scope: tab,
|
|
1090
|
-
page,
|
|
1091
|
-
perPage: DETAIL_BATCH_PAGE_SIZE,
|
|
1092
|
-
loading: true,
|
|
1093
|
-
},
|
|
1094
|
-
};
|
|
1095
|
-
render();
|
|
1096
|
-
|
|
1097
|
-
try {
|
|
1098
|
-
const qs = new URLSearchParams({ page: String(page), perPage: String(DETAIL_BATCH_PAGE_SIZE) });
|
|
1099
|
-
if (query.type) qs.set('type', query.type);
|
|
1100
|
-
if (query.scope) qs.set('scope', query.scope);
|
|
1101
|
-
const batch = await api('/batch/' + encodeURIComponent(detail.batchId) + '?' + qs.toString());
|
|
1102
|
-
state = {
|
|
1103
|
-
...state,
|
|
1104
|
-
detailTab: tab,
|
|
1105
|
-
detailBatch: createDetailBatchState(batch, tab, false),
|
|
1106
|
-
page: 'entries'
|
|
1107
|
-
};
|
|
1108
|
-
render();
|
|
1109
|
-
} catch (error) {
|
|
1110
|
-
state = {
|
|
1111
|
-
...state,
|
|
1112
|
-
detailTab: tab,
|
|
1113
|
-
detailBatch: { ...previous, loading: false }
|
|
1114
|
-
};
|
|
1115
|
-
render();
|
|
1116
|
-
window.alert(error.message);
|
|
1117
|
-
}
|
|
1118
|
-
};
|
|
1119
|
-
|
|
1120
|
-
const addTag = async () => {
|
|
1121
|
-
const input = document.getElementById('new-tag');
|
|
1122
|
-
const value = input && 'value' in input ? String(input.value || '').trim() : '';
|
|
1123
|
-
if (value === '') return;
|
|
1124
|
-
try {
|
|
1125
|
-
await api('/monitoring/' + encodeURIComponent(value), { method: 'POST' });
|
|
1126
|
-
render();
|
|
1127
|
-
} catch (error) {
|
|
1128
|
-
window.alert(error.message);
|
|
1129
|
-
}
|
|
1130
|
-
};
|
|
1131
|
-
|
|
1132
|
-
const removeTag = async (tag) => {
|
|
1133
|
-
try {
|
|
1134
|
-
await api('/monitoring/' + encodeURIComponent(tag), { method: 'DELETE' });
|
|
1135
|
-
render();
|
|
1136
|
-
} catch (error) {
|
|
1137
|
-
window.alert(error.message);
|
|
1138
|
-
}
|
|
1139
|
-
};
|
|
1140
|
-
|
|
1141
|
-
const copyText = async (text) => {
|
|
1142
|
-
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
1143
|
-
await navigator.clipboard.writeText(text);
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
const textarea = document.createElement('textarea');
|
|
1148
|
-
textarea.value = text;
|
|
1149
|
-
textarea.setAttribute('readonly', 'true');
|
|
1150
|
-
textarea.style.position = 'absolute';
|
|
1151
|
-
textarea.style.left = '-9999px';
|
|
1152
|
-
document.body.appendChild(textarea);
|
|
1153
|
-
textarea.select();
|
|
1154
|
-
document.execCommand('copy');
|
|
1155
|
-
document.body.removeChild(textarea);
|
|
1156
|
-
};
|
|
1157
|
-
|
|
1158
|
-
const copyCode = async (copyId, button) => {
|
|
1159
|
-
const payload = copyPayloads.get(copyId);
|
|
1160
|
-
if (typeof payload !== 'string') return;
|
|
1161
|
-
try {
|
|
1162
|
-
await copyText(payload);
|
|
1163
|
-
if (button instanceof HTMLElement) {
|
|
1164
|
-
button.dataset.copied = 'true';
|
|
1165
|
-
window.setTimeout(() => {
|
|
1166
|
-
if (button.dataset.copied === 'true') delete button.dataset.copied;
|
|
1167
|
-
}, 1200);
|
|
1168
|
-
}
|
|
1169
|
-
} catch (error) {
|
|
1170
|
-
window.alert(error.message || 'Failed to copy block');
|
|
1171
|
-
}
|
|
1172
|
-
};
|
|
1173
|
-
|
|
1174
|
-
document.addEventListener('click', (event) => {
|
|
1175
|
-
const target = event.target instanceof Element ? event.target.closest('[data-action],[data-page],#theme-toggle') : null;
|
|
1176
|
-
if (!target) return;
|
|
1177
|
-
|
|
1178
|
-
if (target.id === 'theme-toggle') {
|
|
1179
|
-
setTheme(document.documentElement.dataset.theme === 'light' ? 'dark' : 'light');
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
if (target.hasAttribute('data-page') && !target.hasAttribute('data-action')) {
|
|
1184
|
-
setPage(String(target.getAttribute('data-page') || 'overview'));
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
const action = target.getAttribute('data-action');
|
|
1189
|
-
if (action === 'go-page') { setPage(String(target.getAttribute('data-page') || 'overview')); return; }
|
|
1190
|
-
if (action === 'type-shortcut') { setTypeShortcut(String(target.getAttribute('data-type') || '')); return; }
|
|
1191
|
-
if (action === 'filter-tag') {
|
|
1192
|
-
if (target instanceof HTMLAnchorElement && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)) return;
|
|
1193
|
-
event.preventDefault();
|
|
1194
|
-
filterByTag(String(target.getAttribute('data-tag') || ''));
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
|
-
if (action === 'detail-tab') {
|
|
1198
|
-
const tab = String(target.getAttribute('data-tab') || 'summary');
|
|
1199
|
-
if (Object.prototype.hasOwnProperty.call(DETAIL_BATCH_SCOPE_BY_TAB, tab)) {
|
|
1200
|
-
loadDetailBatchTab(tab, 1);
|
|
1201
|
-
return;
|
|
1202
|
-
}
|
|
1203
|
-
state = { ...state, detailTab: tab };
|
|
1204
|
-
render();
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
if (action === 'clear-all') { clearAll(); return; }
|
|
1208
|
-
if (action === 'show-detail') { showDetail(String(target.getAttribute('data-uuid') || '')); return; }
|
|
1209
|
-
if (action === 'close-detail') { state = { ...state, detail: null, detailBatch: null, detailTab: 'summary' }; render(); return; }
|
|
1210
|
-
if (action === 'detail-batch-prev' || action === 'detail-batch-next') {
|
|
1211
|
-
const detailBatch = detailBatchState();
|
|
1212
|
-
const nextPage = action === 'detail-batch-prev' ? Math.max(1, detailBatch.page - 1) : detailBatch.page + 1;
|
|
1213
|
-
if (detailBatch.scope !== '') {
|
|
1214
|
-
loadDetailBatchTab(detailBatch.scope, nextPage);
|
|
1215
|
-
}
|
|
1216
|
-
return;
|
|
1217
|
-
}
|
|
1218
|
-
if (action === 'page-prev') { state = { ...state, entriesPage: Math.max(1, state.entriesPage - 1) }; render(); return; }
|
|
1219
|
-
if (action === 'page-next') { state = { ...state, entriesPage: state.entriesPage + 1 }; render(); return; }
|
|
1220
|
-
if (action === 'clear-filters') { state = { ...state, detail: null, detailBatch: null, detailTab: 'summary', entriesPage: 1, entriesFilter: { type: '', tag: '', batchId: '' } }; render(); return; }
|
|
1221
|
-
if (action === 'add-tag') { addTag(); return; }
|
|
1222
|
-
if (action === 'remove-tag') { removeTag(String(target.getAttribute('data-tag') || '')); return; }
|
|
1223
|
-
if (action === 'copy-code') { copyCode(String(target.getAttribute('data-copy-id') || ''), target); }
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
document.addEventListener('input', (event) => {
|
|
1227
|
-
const target = event.target;
|
|
1228
|
-
if (!(target instanceof HTMLElement)) return;
|
|
1229
|
-
if (target.id === 'f-tag' || target.id === 'f-batch') syncFilters();
|
|
1230
|
-
});
|
|
1231
|
-
|
|
1232
|
-
document.addEventListener('change', (event) => {
|
|
1233
|
-
const target = event.target;
|
|
1234
|
-
if (!(target instanceof HTMLElement)) return;
|
|
1235
|
-
if (target.id === 'f-type') syncFilters();
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
render();
|
|
1239
|
-
})();
|
|
1240
|
-
</script>
|
|
1241
|
-
</body>
|
|
1242
|
-
</html>`;
|
|
1243
|
-
|
|
1244
|
-
const buildDashboardHtml = (basePath: string, projectName?: string): string => {
|
|
1245
|
-
const resolvedProjectName = projectName && projectName.trim() !== '' ? projectName : 'ZinTrust';
|
|
1246
|
-
const resolvedTitle = `ZinTrust Trace - ${resolvedProjectName}`;
|
|
1247
|
-
|
|
1248
|
-
return DASHBOARD_DOCUMENT.replace('__TRACE_FAVICON__', encodeSvgDataUri(BRAND_SVG))
|
|
1249
|
-
.replace('__TRACE_TITLE__', resolvedTitle)
|
|
1250
|
-
.replace('__TRACE_LOGO__', BRAND_SVG)
|
|
1251
|
-
.replaceAll('__TRACE_PROJECT_NAME__', resolvedProjectName)
|
|
1252
|
-
.replace('__TRACE_SUN_ICON__', JSON.stringify(SUN_ICON))
|
|
1253
|
-
.replace('__TRACE_MOON_ICON__', JSON.stringify(MOON_ICON))
|
|
1254
|
-
.replace('__TRACE_COPY_ICON__', JSON.stringify(COPY_ICON))
|
|
1255
|
-
.replace('__TRACE_DISCLOSURE_ICON__', JSON.stringify(DISCLOSURE_ICON))
|
|
1256
|
-
.replace('__TRACE_JSON_REGEX__', JSON.stringify(JSON_HIGHLIGHT_PATTERN))
|
|
1257
|
-
.replace('__TRACE_SQL_REGEX__', JSON.stringify(SQL_HIGHLIGHT_PATTERN))
|
|
1258
|
-
.replace('__TRACE_BASE_PATH_LABEL__', basePath)
|
|
1259
|
-
.replace('__TRACE_BASE_PATH_JSON__', JSON.stringify(basePath));
|
|
1260
|
-
};
|
|
1261
|
-
|
|
1262
|
-
export { buildDashboardHtml };
|