@zintrust/trace 0.4.96 → 0.5.1
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/dist/build-manifest.json +16 -16
- package/dist/config.js +1 -1
- package/dist/dashboard/handlers.js +133 -6
- package/dist/dashboard/ui.js +161 -26
- package/dist/storage/TraceStorage.js +157 -3
- package/dist/types.d.ts +17 -0
- package/dist/watchers/HttpClientWatcher.d.ts +1 -1
- package/dist/watchers/HttpClientWatcher.js +29 -9
- package/package.json +3 -3
- package/src/config.ts +1 -1
- package/src/dashboard/handlers.ts +163 -7
- package/src/dashboard/ui.ts +161 -26
- package/src/storage/TraceStorage.ts +194 -4
- package/src/types.ts +19 -0
- package/src/watchers/HttpClientWatcher.ts +49 -17
package/dist/build-manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"buildDate": "2026-04-12T15:38:51.982Z",
|
|
5
5
|
"buildEnvironment": {
|
|
6
6
|
"node": "v22.22.1",
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"arch": "arm64"
|
|
9
9
|
},
|
|
10
10
|
"git": {
|
|
11
|
-
"commit": "
|
|
11
|
+
"commit": "d9f9cf02",
|
|
12
12
|
"branch": "release"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"files": {
|
|
24
24
|
"build-manifest.json": {
|
|
25
25
|
"size": 14739,
|
|
26
|
-
"sha256": "
|
|
26
|
+
"sha256": "ccb926d22667a613e35b97c316d5924b223b5522c5f7deab259d99343fe4c8d8"
|
|
27
27
|
},
|
|
28
28
|
"cli-register.d.ts": {
|
|
29
29
|
"size": 255,
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"sha256": "b034cbef0c71fb868071363624ef7a9f8d7acc20f8be8c895dd5db5a75e81f37"
|
|
39
39
|
},
|
|
40
40
|
"config.js": {
|
|
41
|
-
"size":
|
|
42
|
-
"sha256": "
|
|
41
|
+
"size": 9272,
|
|
42
|
+
"sha256": "d23145038d47ce94a51394088b2b6a3138ec232802bde7bd521d347c9bdfb310"
|
|
43
43
|
},
|
|
44
44
|
"context.d.ts": {
|
|
45
45
|
"size": 596,
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"sha256": "430f5b294d960e13e2ec39ed5c22f6655f85c46c2de9455c222505c67298ec6a"
|
|
55
55
|
},
|
|
56
56
|
"dashboard/handlers.js": {
|
|
57
|
-
"size":
|
|
58
|
-
"sha256": "
|
|
57
|
+
"size": 9192,
|
|
58
|
+
"sha256": "ebc592e84772b93c820c76f1834e1131a05c19df3bada7b75cf46bcd5f6732fc"
|
|
59
59
|
},
|
|
60
60
|
"dashboard/routes.d.ts": {
|
|
61
61
|
"size": 997,
|
|
@@ -70,16 +70,16 @@
|
|
|
70
70
|
"sha256": "4862b41e0477f01afa0dbb446d4553b65c22ed774cd1e2db3489059ced392f94"
|
|
71
71
|
},
|
|
72
72
|
"dashboard/ui.js": {
|
|
73
|
-
"size":
|
|
74
|
-
"sha256": "
|
|
73
|
+
"size": 75051,
|
|
74
|
+
"sha256": "941c8647e778f67b8ab3f37b9272b914e5da4f35e07511ff1112287a0620b6da"
|
|
75
75
|
},
|
|
76
76
|
"index.d.ts": {
|
|
77
77
|
"size": 2537,
|
|
78
78
|
"sha256": "1707d26322dbad17f6bf85938ae6fe2477e84c7fed3333760ce8d6eadfaaffd2"
|
|
79
79
|
},
|
|
80
80
|
"index.js": {
|
|
81
|
-
"size":
|
|
82
|
-
"sha256": "
|
|
81
|
+
"size": 3324,
|
|
82
|
+
"sha256": "8a10c92829328ecb61fadb66c54768b52d72956fb196ad9a0087ba1ad9b17cbd"
|
|
83
83
|
},
|
|
84
84
|
"migrations/20260331000001_create_zin_trace_entries_table.d.ts": {
|
|
85
85
|
"size": 304,
|
|
@@ -322,12 +322,12 @@
|
|
|
322
322
|
"sha256": "f318cdeec954ce0bba97be1dc11a6dff935b081e6b6a417c614be1934fa47f04"
|
|
323
323
|
},
|
|
324
324
|
"watchers/HttpClientWatcher.d.ts": {
|
|
325
|
-
"size":
|
|
326
|
-
"sha256": "
|
|
325
|
+
"size": 346,
|
|
326
|
+
"sha256": "dfb13bba526d5338e4dcd7a5aa0f72a61a03d9e2f6a250250c0bb8f0054c9712"
|
|
327
327
|
},
|
|
328
328
|
"watchers/HttpClientWatcher.js": {
|
|
329
|
-
"size":
|
|
330
|
-
"sha256": "
|
|
329
|
+
"size": 5829,
|
|
330
|
+
"sha256": "3921cfb79cd5ff0da8af90ee66a5ad3b76a397e60df808885d6ecc84b5269810"
|
|
331
331
|
},
|
|
332
332
|
"watchers/HttpWatcher.d.ts": {
|
|
333
333
|
"size": 96,
|
package/dist/config.js
CHANGED
|
@@ -73,7 +73,7 @@ const mergeClientRequestCaptureRule = (base, override) => {
|
|
|
73
73
|
const collectClientRequestSourceKeys = (base, override) => {
|
|
74
74
|
const overrideSources = override?.sources ?? {};
|
|
75
75
|
const sourceKeys = new Set([
|
|
76
|
-
...Object.keys(isObjectValue(base) ? base.sources ?? {} : {}),
|
|
76
|
+
...Object.keys(isObjectValue(base) ? (base.sources ?? {}) : {}),
|
|
77
77
|
...Object.keys(overrideSources),
|
|
78
78
|
]);
|
|
79
79
|
return [...sourceKeys];
|
|
@@ -38,24 +38,140 @@ const getNumericQueryParam = (req, key) => {
|
|
|
38
38
|
}
|
|
39
39
|
return undefined;
|
|
40
40
|
};
|
|
41
|
+
const DEFAULT_PER_PAGE = 50;
|
|
42
|
+
const MAX_PER_PAGE = 100;
|
|
43
|
+
const DEFAULT_REQUEST_PER_PAGE = 25;
|
|
44
|
+
const MAX_REQUEST_PER_PAGE = 50;
|
|
45
|
+
const DEFAULT_BATCH_PER_PAGE = 10;
|
|
46
|
+
const MAX_BATCH_PER_PAGE = 25;
|
|
47
|
+
const SUMMARY_TEXT_LIMIT = 280;
|
|
48
|
+
const SUMMARY_ARRAY_LIMIT = 10;
|
|
49
|
+
const REQUEST_BATCH_DEFAULT_EXCLUDED_TYPES = [
|
|
50
|
+
'request',
|
|
51
|
+
'query',
|
|
52
|
+
'middleware',
|
|
53
|
+
'model',
|
|
54
|
+
'log',
|
|
55
|
+
'exception',
|
|
56
|
+
'client_request',
|
|
57
|
+
'cache',
|
|
58
|
+
];
|
|
59
|
+
const truncateText = (value, limit = SUMMARY_TEXT_LIMIT) => value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 3))}...`;
|
|
60
|
+
const compactValue = (value) => {
|
|
61
|
+
if (typeof value === 'string') {
|
|
62
|
+
return truncateText(value);
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'number' ||
|
|
65
|
+
typeof value === 'boolean' ||
|
|
66
|
+
value === null ||
|
|
67
|
+
value === undefined) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return value.slice(0, SUMMARY_ARRAY_LIMIT).map((item) => {
|
|
72
|
+
if (typeof item === 'string') {
|
|
73
|
+
return truncateText(item);
|
|
74
|
+
}
|
|
75
|
+
if (typeof item === 'number' || typeof item === 'boolean' || item === null) {
|
|
76
|
+
return item;
|
|
77
|
+
}
|
|
78
|
+
return '[complex]';
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
};
|
|
83
|
+
const pickCompactContent = (content, keys) => {
|
|
84
|
+
if (typeof content !== 'object' || content === null || Array.isArray(content)) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
const source = content;
|
|
88
|
+
const compact = {};
|
|
89
|
+
for (const key of keys) {
|
|
90
|
+
const value = compactValue(source[key]);
|
|
91
|
+
if (value !== undefined) {
|
|
92
|
+
compact[key] = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return compact;
|
|
96
|
+
};
|
|
97
|
+
const COMPACT_ENTRY_KEYS = {
|
|
98
|
+
request: [
|
|
99
|
+
'method',
|
|
100
|
+
'uri',
|
|
101
|
+
'responseStatus',
|
|
102
|
+
'duration',
|
|
103
|
+
'memory',
|
|
104
|
+
'middleware',
|
|
105
|
+
'hostname',
|
|
106
|
+
'userId',
|
|
107
|
+
],
|
|
108
|
+
query: ['connection', 'sql', 'time', 'duration', 'slow', 'hash', 'hostname'],
|
|
109
|
+
exception: ['class', 'file', 'line', 'message', 'occurrences', 'hostname', 'userId'],
|
|
110
|
+
log: ['level', 'message', 'hostname'],
|
|
111
|
+
job: ['status', 'connection', 'queue', 'name', 'tries', 'timeout', 'hostname'],
|
|
112
|
+
cache: ['operation', 'key', 'hit', 'store', 'payloadLogged', 'ttl', 'duration', 'hostname'],
|
|
113
|
+
schedule: ['name', 'expression', 'status', 'duration', 'hostname'],
|
|
114
|
+
mail: ['to', 'subject', 'template', 'hostname'],
|
|
115
|
+
auth: ['event', 'userId', 'hostname'],
|
|
116
|
+
event: ['name', 'listenerCount', 'hostname'],
|
|
117
|
+
model: ['action', 'model', 'id', 'hostname'],
|
|
118
|
+
notification: ['channels', 'notifiable', 'notification', 'message', 'hostname'],
|
|
119
|
+
redis: ['command', 'duration', 'hostname'],
|
|
120
|
+
gate: ['ability', 'result', 'userId', 'subject', 'hostname'],
|
|
121
|
+
middleware: ['name', 'event', 'duration', 'hostname'],
|
|
122
|
+
command: ['name', 'exitCode', 'duration', 'hostname'],
|
|
123
|
+
batch: ['name', 'total', 'processed', 'failed', 'status', 'hostname'],
|
|
124
|
+
dump: ['file', 'line', 'hostname'],
|
|
125
|
+
view: ['template', 'duration', 'hostname'],
|
|
126
|
+
client_request: ['source', 'method', 'url', 'responseStatus', 'error', 'duration', 'hostname'],
|
|
127
|
+
};
|
|
128
|
+
const compactEntryContent = (entry) => pickCompactContent(entry.content, COMPACT_ENTRY_KEYS[entry.type]);
|
|
129
|
+
const estimateContentBytes = (content) => {
|
|
130
|
+
try {
|
|
131
|
+
return new TextEncoder().encode(JSON.stringify(content)).length;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const compactListEntry = (entry) => ({
|
|
138
|
+
...entry,
|
|
139
|
+
content: compactEntryContent(entry),
|
|
140
|
+
hasDetails: true,
|
|
141
|
+
contentBytes: estimateContentBytes(entry.content),
|
|
142
|
+
});
|
|
143
|
+
const resolvePerPage = (req, type) => {
|
|
144
|
+
const isRequestList = type === 'request';
|
|
145
|
+
const fallback = isRequestList ? DEFAULT_REQUEST_PER_PAGE : DEFAULT_PER_PAGE;
|
|
146
|
+
const limit = isRequestList ? MAX_REQUEST_PER_PAGE : MAX_PER_PAGE;
|
|
147
|
+
return Math.max(1, Math.min(qpInt(req, 'perPage', fallback), limit));
|
|
148
|
+
};
|
|
41
149
|
// ---------------------------------------------------------------------------
|
|
42
150
|
// Entry handlers
|
|
43
151
|
// ---------------------------------------------------------------------------
|
|
44
152
|
export async function listEntries(req, res) {
|
|
45
153
|
const storage = getStorage(res);
|
|
46
154
|
if (storage !== null) {
|
|
155
|
+
const type = qp(req, 'type');
|
|
47
156
|
const opts = {
|
|
48
|
-
type
|
|
157
|
+
type,
|
|
49
158
|
tag: qp(req, 'tag'),
|
|
50
159
|
batchId: qp(req, 'batchId'),
|
|
51
160
|
from: getNumericQueryParam(req, 'from'),
|
|
52
161
|
to: getNumericQueryParam(req, 'to'),
|
|
53
|
-
page: qpInt(req, 'page', 1),
|
|
54
|
-
perPage:
|
|
162
|
+
page: Math.max(1, qpInt(req, 'page', 1)),
|
|
163
|
+
perPage: resolvePerPage(req, type),
|
|
164
|
+
summary: true,
|
|
55
165
|
};
|
|
56
166
|
try {
|
|
57
167
|
const result = await storage.queryEntries(opts);
|
|
58
|
-
res.json({
|
|
168
|
+
res.json({
|
|
169
|
+
ok: true,
|
|
170
|
+
data: result.data.map(compactListEntry),
|
|
171
|
+
total: result.total,
|
|
172
|
+
page: opts.page,
|
|
173
|
+
perPage: opts.perPage,
|
|
174
|
+
});
|
|
59
175
|
}
|
|
60
176
|
catch (err) {
|
|
61
177
|
res.setStatus(500).json({ error: err.message });
|
|
@@ -91,8 +207,19 @@ export async function getBatch(req, res) {
|
|
|
91
207
|
const batchId = req.getParam('batchId');
|
|
92
208
|
if (batchId) {
|
|
93
209
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
210
|
+
const scope = qp(req, 'scope');
|
|
211
|
+
const type = qp(req, 'type');
|
|
212
|
+
const countsOnly = qp(req, 'countsOnly') === 'true';
|
|
213
|
+
const page = Math.max(1, qpInt(req, 'page', 1));
|
|
214
|
+
const perPage = Math.max(1, Math.min(qpInt(req, 'perPage', DEFAULT_BATCH_PER_PAGE), MAX_BATCH_PER_PAGE));
|
|
215
|
+
const result = await storage.queryBatchEntries(batchId, {
|
|
216
|
+
type,
|
|
217
|
+
excludeTypes: scope === 'other' ? REQUEST_BATCH_DEFAULT_EXCLUDED_TYPES : undefined,
|
|
218
|
+
page,
|
|
219
|
+
perPage,
|
|
220
|
+
countsOnly,
|
|
221
|
+
});
|
|
222
|
+
res.json({ ok: true, ...result });
|
|
96
223
|
return;
|
|
97
224
|
}
|
|
98
225
|
catch (err) {
|
package/dist/dashboard/ui.js
CHANGED
|
@@ -133,9 +133,6 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
133
133
|
const MOON_ICON = __TRACE_MOON_ICON__;
|
|
134
134
|
const COPY_ICON = __TRACE_COPY_ICON__;
|
|
135
135
|
const DISCLOSURE_ICON = __TRACE_DISCLOSURE_ICON__;
|
|
136
|
-
.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}
|
|
137
|
-
.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)}
|
|
138
|
-
.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}.trace-summary-icon{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--muted);flex:none;transition:transform .16s ease,color .16s ease}.trace-summary-icon svg{width:14px;height:14px;display:block}.trace-disclosure[open] .trace-summary-icon{transform:rotate(90deg);color:var(--accent)}
|
|
139
136
|
const JSON_HIGHLIGHT_PATTERN = new RegExp(__TRACE_JSON_REGEX__, 'g');
|
|
140
137
|
const SQL_HIGHLIGHT_PATTERN = new RegExp(__TRACE_SQL_REGEX__, 'gim');
|
|
141
138
|
const ENTRY_TYPES = ['request','query','exception','log','job','cache','schedule','mail','auth','event','model','notification','redis','gate','middleware','command','batch','dump','view','client_request'];
|
|
@@ -166,6 +163,28 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
166
163
|
|
|
167
164
|
let state = createInitialState();
|
|
168
165
|
|
|
166
|
+
const DETAIL_BATCH_PAGE_SIZE = 10;
|
|
167
|
+
const DETAIL_BATCH_SCOPE_BY_TAB = Object.freeze({
|
|
168
|
+
queries: { type: 'query' },
|
|
169
|
+
middleware: { type: 'middleware' },
|
|
170
|
+
models: { type: 'model' },
|
|
171
|
+
logs: { type: 'log' },
|
|
172
|
+
exceptions: { type: 'exception' },
|
|
173
|
+
http: { type: 'client_request' },
|
|
174
|
+
cache: { type: 'cache' },
|
|
175
|
+
other: { scope: 'other' }
|
|
176
|
+
});
|
|
177
|
+
const DETAIL_BATCH_COUNT_TYPES = Object.freeze({
|
|
178
|
+
queries: 'query',
|
|
179
|
+
middleware: 'middleware',
|
|
180
|
+
models: 'model',
|
|
181
|
+
logs: 'log',
|
|
182
|
+
exceptions: 'exception',
|
|
183
|
+
http: 'client_request',
|
|
184
|
+
cache: 'cache'
|
|
185
|
+
});
|
|
186
|
+
const DETAIL_BATCH_OTHER_EXCLUDED_TYPES = ['request','query','middleware','model','log','exception','client_request','cache'];
|
|
187
|
+
|
|
169
188
|
let copySequence = 0;
|
|
170
189
|
const copyPayloads = new Map();
|
|
171
190
|
|
|
@@ -322,9 +341,40 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
322
341
|
return raw === '' ? '-' : escapeHtml(raw.slice(0, 8));
|
|
323
342
|
};
|
|
324
343
|
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
344
|
+
const createDetailBatchState = (payload, scope = '', loading = false) => ({
|
|
345
|
+
counts: payload && typeof payload.counts === 'object' && payload.counts !== null ? payload.counts : {},
|
|
346
|
+
entries: payload && Array.isArray(payload.entries) ? payload.entries : [],
|
|
347
|
+
total: payload && Number.isFinite(Number(payload.total)) ? Number(payload.total) : 0,
|
|
348
|
+
page: payload && Number.isFinite(Number(payload.page)) && Number(payload.page) > 0 ? Number(payload.page) : 1,
|
|
349
|
+
perPage: payload && Number.isFinite(Number(payload.perPage)) && Number(payload.perPage) > 0 ? Number(payload.perPage) : DETAIL_BATCH_PAGE_SIZE,
|
|
350
|
+
scope,
|
|
351
|
+
loading
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const detailBatchState = () => {
|
|
355
|
+
const detailBatch = state.detailBatch;
|
|
356
|
+
if (!detailBatch || typeof detailBatch !== 'object' || Array.isArray(detailBatch)) {
|
|
357
|
+
return createDetailBatchState(null);
|
|
358
|
+
}
|
|
359
|
+
return createDetailBatchState(detailBatch, typeof detailBatch.scope === 'string' ? detailBatch.scope : '', detailBatch.loading === true);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const batchEntries = () => detailBatchState().entries;
|
|
363
|
+
const batchCounts = () => detailBatchState().counts;
|
|
364
|
+
const batchCount = (type) => {
|
|
365
|
+
const raw = batchCounts()[type];
|
|
366
|
+
return Number.isFinite(Number(raw)) ? Number(raw) : 0;
|
|
367
|
+
};
|
|
368
|
+
const otherBatchCount = () => Object.entries(batchCounts()).reduce((sum, pair) => {
|
|
369
|
+
return DETAIL_BATCH_OTHER_EXCLUDED_TYPES.includes(pair[0]) ? sum : sum + Number(pair[1] || 0);
|
|
370
|
+
}, 0);
|
|
371
|
+
const resolveDetailBatchQuery = (tab) => DETAIL_BATCH_SCOPE_BY_TAB[tab] || null;
|
|
372
|
+
const resolveDetailBatchCount = (tab) => {
|
|
373
|
+
if (tab === 'other') return otherBatchCount();
|
|
374
|
+
const type = DETAIL_BATCH_COUNT_TYPES[tab];
|
|
375
|
+
return typeof type === 'string' ? batchCount(type) : 0;
|
|
376
|
+
};
|
|
377
|
+
const hasRequestTrace = () => Boolean(state.detail && state.detail.type === 'request');
|
|
328
378
|
|
|
329
379
|
const prettyJson = (value) => {
|
|
330
380
|
try {
|
|
@@ -683,6 +733,27 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
683
733
|
}).join('') + '</div>';
|
|
684
734
|
};
|
|
685
735
|
|
|
736
|
+
const renderDetailBatchPanel = (tab) => {
|
|
737
|
+
const detailBatch = detailBatchState();
|
|
738
|
+
const count = resolveDetailBatchCount(tab);
|
|
739
|
+
if (detailBatch.loading && detailBatch.scope === tab) {
|
|
740
|
+
return '<p class="trace-note">Loading related entries...</p>';
|
|
741
|
+
}
|
|
742
|
+
if (count === 0) {
|
|
743
|
+
return '<p class="trace-note">No related entries captured.</p>';
|
|
744
|
+
}
|
|
745
|
+
if (detailBatch.scope !== tab) {
|
|
746
|
+
return '<p class="trace-note">Open this tab to load the first page of related entries.</p>';
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const totalPages = Math.max(1, Math.ceil(detailBatch.total / Math.max(1, detailBatch.perPage)));
|
|
750
|
+
|
|
751
|
+
return [
|
|
752
|
+
renderTraceItems(batchEntries()),
|
|
753
|
+
'<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>'
|
|
754
|
+
].join('');
|
|
755
|
+
};
|
|
756
|
+
|
|
686
757
|
const renderRequestTrace = (main) => {
|
|
687
758
|
const entry = state.detail;
|
|
688
759
|
const content = entry && entry.content ? entry.content : {};
|
|
@@ -691,17 +762,16 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
691
762
|
{ id: 'payload', label: 'Payload' },
|
|
692
763
|
{ id: 'headers', label: 'Headers' },
|
|
693
764
|
{ id: 'response', label: 'Response' },
|
|
694
|
-
{ id: 'queries', label: 'Queries', count:
|
|
695
|
-
{ id: 'middleware', label: 'Middleware', count:
|
|
696
|
-
{ id: 'models', label: 'Models', count:
|
|
697
|
-
{ id: 'logs', label: 'Logs', count:
|
|
698
|
-
{ id: 'exceptions', label: 'Exceptions', count:
|
|
699
|
-
{ id: 'http', label: 'HTTP', count:
|
|
700
|
-
{ id: 'cache', label: 'Cache', count:
|
|
701
|
-
{ id: 'other', label: 'Other', count:
|
|
765
|
+
{ id: 'queries', label: 'Queries', count: resolveDetailBatchCount('queries') },
|
|
766
|
+
{ id: 'middleware', label: 'Middleware', count: resolveDetailBatchCount('middleware') },
|
|
767
|
+
{ id: 'models', label: 'Models', count: resolveDetailBatchCount('models') },
|
|
768
|
+
{ id: 'logs', label: 'Logs', count: resolveDetailBatchCount('logs') },
|
|
769
|
+
{ id: 'exceptions', label: 'Exceptions', count: resolveDetailBatchCount('exceptions') },
|
|
770
|
+
{ id: 'http', label: 'HTTP', count: resolveDetailBatchCount('http') },
|
|
771
|
+
{ id: 'cache', label: 'Cache', count: resolveDetailBatchCount('cache') },
|
|
772
|
+
{ id: 'other', label: 'Other', count: resolveDetailBatchCount('other') }
|
|
702
773
|
];
|
|
703
774
|
const currentTab = traceTabs.some((tab) => tab.id === state.detailTab) ? state.detailTab : 'summary';
|
|
704
|
-
const otherEntries = batchEntries().filter((item) => !['request','query','middleware','model','log','exception','client_request','cache'].includes(item.type));
|
|
705
775
|
const panels = {
|
|
706
776
|
summary: [
|
|
707
777
|
'<div class="detail-grid">',
|
|
@@ -728,14 +798,14 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
728
798
|
payload: detailJson(content.payload || {}, 'Payload Json'),
|
|
729
799
|
headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
730
800
|
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: 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>',
|
|
731
|
-
queries:
|
|
732
|
-
middleware:
|
|
733
|
-
models:
|
|
734
|
-
logs:
|
|
735
|
-
exceptions:
|
|
736
|
-
http:
|
|
737
|
-
cache:
|
|
738
|
-
other:
|
|
801
|
+
queries: renderDetailBatchPanel('queries'),
|
|
802
|
+
middleware: renderDetailBatchPanel('middleware'),
|
|
803
|
+
models: renderDetailBatchPanel('models'),
|
|
804
|
+
logs: renderDetailBatchPanel('logs'),
|
|
805
|
+
exceptions: renderDetailBatchPanel('exceptions'),
|
|
806
|
+
http: renderDetailBatchPanel('http'),
|
|
807
|
+
cache: renderDetailBatchPanel('cache'),
|
|
808
|
+
other: renderDetailBatchPanel('other')
|
|
739
809
|
};
|
|
740
810
|
|
|
741
811
|
main.innerHTML = [
|
|
@@ -955,8 +1025,8 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
955
1025
|
const entry = detailResult.entry;
|
|
956
1026
|
let detailBatch = null;
|
|
957
1027
|
if (entry.type === 'request' && entry.batchId) {
|
|
958
|
-
const batch = await api('/batch/' + encodeURIComponent(entry.batchId));
|
|
959
|
-
detailBatch = batch
|
|
1028
|
+
const batch = await api('/batch/' + encodeURIComponent(entry.batchId) + '?countsOnly=true');
|
|
1029
|
+
detailBatch = createDetailBatchState(batch);
|
|
960
1030
|
}
|
|
961
1031
|
state = { ...state, detail: entry, detailBatch, detailTab: 'summary', page: 'entries' };
|
|
962
1032
|
render();
|
|
@@ -965,6 +1035,54 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
965
1035
|
}
|
|
966
1036
|
};
|
|
967
1037
|
|
|
1038
|
+
const loadDetailBatchTab = async (tab, page = 1) => {
|
|
1039
|
+
const detail = state.detail;
|
|
1040
|
+
if (!detail || detail.type !== 'request' || !detail.batchId) return;
|
|
1041
|
+
|
|
1042
|
+
const query = resolveDetailBatchQuery(tab);
|
|
1043
|
+
if (!query) {
|
|
1044
|
+
state = { ...state, detailTab: tab };
|
|
1045
|
+
render();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const previous = detailBatchState();
|
|
1050
|
+
state = {
|
|
1051
|
+
...state,
|
|
1052
|
+
detailTab: tab,
|
|
1053
|
+
detailBatch: {
|
|
1054
|
+
...previous,
|
|
1055
|
+
scope: tab,
|
|
1056
|
+
page,
|
|
1057
|
+
perPage: DETAIL_BATCH_PAGE_SIZE,
|
|
1058
|
+
loading: true,
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
render();
|
|
1062
|
+
|
|
1063
|
+
try {
|
|
1064
|
+
const qs = new URLSearchParams({ page: String(page), perPage: String(DETAIL_BATCH_PAGE_SIZE) });
|
|
1065
|
+
if (query.type) qs.set('type', query.type);
|
|
1066
|
+
if (query.scope) qs.set('scope', query.scope);
|
|
1067
|
+
const batch = await api('/batch/' + encodeURIComponent(detail.batchId) + '?' + qs.toString());
|
|
1068
|
+
state = {
|
|
1069
|
+
...state,
|
|
1070
|
+
detailTab: tab,
|
|
1071
|
+
detailBatch: createDetailBatchState(batch, tab, false),
|
|
1072
|
+
page: 'entries'
|
|
1073
|
+
};
|
|
1074
|
+
render();
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
state = {
|
|
1077
|
+
...state,
|
|
1078
|
+
detailTab: tab,
|
|
1079
|
+
detailBatch: { ...previous, loading: false }
|
|
1080
|
+
};
|
|
1081
|
+
render();
|
|
1082
|
+
window.alert(error.message);
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
|
|
968
1086
|
const addTag = async () => {
|
|
969
1087
|
const input = document.getElementById('new-tag');
|
|
970
1088
|
const value = input && 'value' in input ? String(input.value || '').trim() : '';
|
|
@@ -1042,10 +1160,27 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
1042
1160
|
filterByTag(String(target.getAttribute('data-tag') || ''));
|
|
1043
1161
|
return;
|
|
1044
1162
|
}
|
|
1045
|
-
if (action === 'detail-tab') {
|
|
1163
|
+
if (action === 'detail-tab') {
|
|
1164
|
+
const tab = String(target.getAttribute('data-tab') || 'summary');
|
|
1165
|
+
if (Object.prototype.hasOwnProperty.call(DETAIL_BATCH_SCOPE_BY_TAB, tab)) {
|
|
1166
|
+
loadDetailBatchTab(tab, 1);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
state = { ...state, detailTab: tab };
|
|
1170
|
+
render();
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1046
1173
|
if (action === 'clear-all') { clearAll(); return; }
|
|
1047
1174
|
if (action === 'show-detail') { showDetail(String(target.getAttribute('data-uuid') || '')); return; }
|
|
1048
1175
|
if (action === 'close-detail') { state = { ...state, detail: null, detailBatch: null, detailTab: 'summary' }; render(); return; }
|
|
1176
|
+
if (action === 'detail-batch-prev' || action === 'detail-batch-next') {
|
|
1177
|
+
const detailBatch = detailBatchState();
|
|
1178
|
+
const nextPage = action === 'detail-batch-prev' ? Math.max(1, detailBatch.page - 1) : detailBatch.page + 1;
|
|
1179
|
+
if (detailBatch.scope !== '') {
|
|
1180
|
+
loadDetailBatchTab(detailBatch.scope, nextPage);
|
|
1181
|
+
}
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1049
1184
|
if (action === 'page-prev') { state = { ...state, entriesPage: Math.max(1, state.entriesPage - 1) }; render(); return; }
|
|
1050
1185
|
if (action === 'page-next') { state = { ...state, entriesPage: state.entriesPage + 1 }; render(); return; }
|
|
1051
1186
|
if (action === 'clear-filters') { state = { ...state, detail: null, detailBatch: null, detailTab: 'summary', entriesPage: 1, entriesFilter: { type: '', tag: '', batchId: '' } }; render(); return; }
|