@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.
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.4.96",
4
- "buildDate": "2026-04-11T20:49:58.121Z",
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": "ddf9b233",
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": "b57ad8dd90ba688df31448cae52218fcfc3be48d533c55a06db0247741f30c8d"
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": 9270,
42
- "sha256": "346361028d355391f3068c340db559dce558145e173bf248aa8d1c6328575de3"
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": 5390,
58
- "sha256": "a8644a932c2da07231098f9adc31cfca00727b448524107ce23ad87a239f53f3"
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": 79698,
74
- "sha256": "60295315a1fbbfd02018a971399739971252ff16f252ddfea0ea43a562aa305f"
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": 3325,
82
- "sha256": "fc1e557217d22f31fa29611bebd54d9a0d93393f9a425283f238d91a30b216c9"
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": 341,
326
- "sha256": "7e20bd9240de2165def5d90ec529e4da4e0a7302bc855bd2fb873f8b71d0182f"
325
+ "size": 346,
326
+ "sha256": "dfb13bba526d5338e4dcd7a5aa0f72a61a03d9e2f6a250250c0bb8f0054c9712"
327
327
  },
328
328
  "watchers/HttpClientWatcher.js": {
329
- "size": 5245,
330
- "sha256": "dcbc10ac6fd583a009bf37c6787738765f5b54bd2a15a1cd6582d0bb2bbb1207"
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: qp(req, '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: Math.min(qpInt(req, 'perPage', 50), 200),
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({ ok: true, ...result, page: opts.page, perPage: opts.perPage });
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 entries = await storage.getBatch(batchId);
95
- res.json({ ok: true, entries });
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) {
@@ -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 batchEntries = () => Array.isArray(state.detailBatch) ? state.detailBatch : [];
326
- const batchEntriesByType = (type) => batchEntries().filter((entry) => entry.type === type);
327
- const hasRequestTrace = () => Boolean(state.detail && state.detail.type === 'request' && batchEntries().length > 0);
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: batchEntriesByType('query').length },
695
- { id: 'middleware', label: 'Middleware', count: batchEntriesByType('middleware').length },
696
- { id: 'models', label: 'Models', count: batchEntriesByType('model').length },
697
- { id: 'logs', label: 'Logs', count: batchEntriesByType('log').length },
698
- { id: 'exceptions', label: 'Exceptions', count: batchEntriesByType('exception').length },
699
- { id: 'http', label: 'HTTP', count: batchEntriesByType('client_request').length },
700
- { id: 'cache', label: 'Cache', count: batchEntriesByType('cache').length },
701
- { id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','middleware','model','log','exception','client_request','cache'].includes(item.type)).length }
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: renderTraceItems(batchEntriesByType('query')),
732
- middleware: renderTraceItems(batchEntriesByType('middleware')),
733
- models: renderTraceItems(batchEntriesByType('model')),
734
- logs: renderTraceItems(batchEntriesByType('log')),
735
- exceptions: renderTraceItems(batchEntriesByType('exception')),
736
- http: renderTraceItems(batchEntriesByType('client_request')),
737
- cache: renderTraceItems(batchEntriesByType('cache')),
738
- other: renderTraceItems(otherEntries)
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.entries || [];
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') { state = { ...state, detailTab: String(target.getAttribute('data-tab') || 'summary') }; render(); return; }
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; }