@zintrust/trace 0.4.96 → 0.5.0

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,127 @@ 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 SUMMARY_TEXT_LIMIT = 280;
46
+ const SUMMARY_ARRAY_LIMIT = 10;
47
+ const truncateText = (value, limit = SUMMARY_TEXT_LIMIT) => value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 3))}...`;
48
+ const compactValue = (value) => {
49
+ if (typeof value === 'string') {
50
+ return truncateText(value);
51
+ }
52
+ if (typeof value === 'number' ||
53
+ typeof value === 'boolean' ||
54
+ value === null ||
55
+ value === undefined) {
56
+ return value;
57
+ }
58
+ if (Array.isArray(value)) {
59
+ return value.slice(0, SUMMARY_ARRAY_LIMIT).map((item) => {
60
+ if (typeof item === 'string') {
61
+ return truncateText(item);
62
+ }
63
+ if (typeof item === 'number' || typeof item === 'boolean' || item === null) {
64
+ return item;
65
+ }
66
+ return '[complex]';
67
+ });
68
+ }
69
+ return undefined;
70
+ };
71
+ const pickCompactContent = (content, keys) => {
72
+ if (typeof content !== 'object' || content === null || Array.isArray(content)) {
73
+ return {};
74
+ }
75
+ const source = content;
76
+ const compact = {};
77
+ for (const key of keys) {
78
+ const value = compactValue(source[key]);
79
+ if (value !== undefined) {
80
+ compact[key] = value;
81
+ }
82
+ }
83
+ return compact;
84
+ };
85
+ const COMPACT_ENTRY_KEYS = {
86
+ request: [
87
+ 'method',
88
+ 'uri',
89
+ 'responseStatus',
90
+ 'duration',
91
+ 'memory',
92
+ 'middleware',
93
+ 'hostname',
94
+ 'userId',
95
+ ],
96
+ query: ['connection', 'sql', 'time', 'duration', 'slow', 'hash', 'hostname'],
97
+ exception: ['class', 'file', 'line', 'message', 'occurrences', 'hostname', 'userId'],
98
+ log: ['level', 'message', 'hostname'],
99
+ job: ['status', 'connection', 'queue', 'name', 'tries', 'timeout', 'hostname'],
100
+ cache: ['operation', 'key', 'hit', 'store', 'payloadLogged', 'ttl', 'duration', 'hostname'],
101
+ schedule: ['name', 'expression', 'status', 'duration', 'hostname'],
102
+ mail: ['to', 'subject', 'template', 'hostname'],
103
+ auth: ['event', 'userId', 'hostname'],
104
+ event: ['name', 'listenerCount', 'hostname'],
105
+ model: ['action', 'model', 'id', 'hostname'],
106
+ notification: ['channels', 'notifiable', 'notification', 'message', 'hostname'],
107
+ redis: ['command', 'duration', 'hostname'],
108
+ gate: ['ability', 'result', 'userId', 'subject', 'hostname'],
109
+ middleware: ['name', 'event', 'duration', 'hostname'],
110
+ command: ['name', 'exitCode', 'duration', 'hostname'],
111
+ batch: ['name', 'total', 'processed', 'failed', 'status', 'hostname'],
112
+ dump: ['file', 'line', 'hostname'],
113
+ view: ['template', 'duration', 'hostname'],
114
+ client_request: ['source', 'method', 'url', 'responseStatus', 'error', 'duration', 'hostname'],
115
+ };
116
+ const compactEntryContent = (entry) => pickCompactContent(entry.content, COMPACT_ENTRY_KEYS[entry.type]);
117
+ const estimateContentBytes = (content) => {
118
+ try {
119
+ return new TextEncoder().encode(JSON.stringify(content)).length;
120
+ }
121
+ catch {
122
+ return undefined;
123
+ }
124
+ };
125
+ const compactListEntry = (entry) => ({
126
+ ...entry,
127
+ content: compactEntryContent(entry),
128
+ hasDetails: true,
129
+ contentBytes: estimateContentBytes(entry.content),
130
+ });
131
+ const resolvePerPage = (req, type) => {
132
+ const isRequestList = type === 'request';
133
+ const fallback = isRequestList ? DEFAULT_REQUEST_PER_PAGE : DEFAULT_PER_PAGE;
134
+ const limit = isRequestList ? MAX_REQUEST_PER_PAGE : MAX_PER_PAGE;
135
+ return Math.max(1, Math.min(qpInt(req, 'perPage', fallback), limit));
136
+ };
41
137
  // ---------------------------------------------------------------------------
42
138
  // Entry handlers
43
139
  // ---------------------------------------------------------------------------
44
140
  export async function listEntries(req, res) {
45
141
  const storage = getStorage(res);
46
142
  if (storage !== null) {
143
+ const type = qp(req, 'type');
47
144
  const opts = {
48
- type: qp(req, 'type'),
145
+ type,
49
146
  tag: qp(req, 'tag'),
50
147
  batchId: qp(req, 'batchId'),
51
148
  from: getNumericQueryParam(req, 'from'),
52
149
  to: getNumericQueryParam(req, 'to'),
53
- page: qpInt(req, 'page', 1),
54
- perPage: Math.min(qpInt(req, 'perPage', 50), 200),
150
+ page: Math.max(1, qpInt(req, 'page', 1)),
151
+ perPage: resolvePerPage(req, type),
55
152
  };
56
153
  try {
57
154
  const result = await storage.queryEntries(opts);
58
- res.json({ ok: true, ...result, page: opts.page, perPage: opts.perPage });
155
+ res.json({
156
+ ok: true,
157
+ data: result.data.map(compactListEntry),
158
+ total: result.total,
159
+ page: opts.page,
160
+ perPage: opts.perPage,
161
+ });
59
162
  }
60
163
  catch (err) {
61
164
  res.setStatus(500).json({ error: err.message });
@@ -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'];
@@ -1,4 +1,4 @@
1
- import type { ClientRequestTraceInput, ITraceWatcher } from '../types';
1
+ import { type ClientRequestTraceInput, type ITraceWatcher } from '../types';
2
2
  declare const emit: ({ source, method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }: ClientRequestTraceInput) => void;
3
3
  export declare const HttpClientWatcher: ITraceWatcher & {
4
4
  emit: typeof emit;
@@ -1,5 +1,6 @@
1
+ import { generateUuid } from '@zintrust/core';
1
2
  import { TraceContext } from '../context.js';
2
- import { EntryType } from '../types.js';
3
+ import { EntryType, } from '../types.js';
3
4
  import { AuthTag } from '../utils/authTag.js';
4
5
  import { redactHeaders, redactUnknown } from '../utils/redact.js';
5
6
  import { RequestFilter } from '../utils/requestFilter.js';
@@ -57,20 +58,40 @@ const buildResponseBody = (responseBody, sourceRule) => {
57
58
  return {};
58
59
  return { responseBody: redactUnknown(responseBody, _redactBodyFields) };
59
60
  };
61
+ const applySource = (content, normalizedSource) => {
62
+ if (normalizedSource !== undefined) {
63
+ content.source = normalizedSource;
64
+ }
65
+ };
66
+ const applyResponseStatus = (content, responseStatus) => {
67
+ if (responseStatus !== undefined) {
68
+ content.responseStatus = responseStatus;
69
+ }
70
+ };
71
+ const applyError = (content, error) => {
72
+ if (typeof error === 'string' && error !== '') {
73
+ content.error = error;
74
+ }
75
+ };
76
+ const mergePartialContent = (content, partial) => {
77
+ Object.assign(content, partial);
78
+ };
60
79
  const buildClientRequestContent = (input, sourceRule, normalizedSource) => {
61
- return {
62
- ...(normalizedSource === undefined ? {} : { source: normalizedSource }),
80
+ const content = {
63
81
  method: input.method.toUpperCase(),
64
82
  url: input.url,
65
- ...buildRequestHeaders(input.requestHeaders, sourceRule),
66
- ...buildRequestBody(input.requestBody, sourceRule),
67
- ...(input.responseStatus === undefined ? {} : { responseStatus: input.responseStatus }),
68
- ...buildResponseHeaders(input.responseHeaders, sourceRule),
69
- ...buildResponseBody(input.responseBody, sourceRule),
70
- ...(typeof input.error === 'string' && input.error !== '' ? { error: input.error } : {}),
83
+ requestHeaders: {},
71
84
  duration: input.duration,
72
85
  hostname: TraceContext.getHostname(),
73
86
  };
87
+ applySource(content, normalizedSource);
88
+ mergePartialContent(content, buildRequestHeaders(input.requestHeaders, sourceRule));
89
+ mergePartialContent(content, buildRequestBody(input.requestBody, sourceRule));
90
+ applyResponseStatus(content, input.responseStatus);
91
+ mergePartialContent(content, buildResponseHeaders(input.responseHeaders, sourceRule));
92
+ mergePartialContent(content, buildResponseBody(input.responseBody, sourceRule));
93
+ applyError(content, input.error);
94
+ return content;
74
95
  };
75
96
  const isWatcherEnabled = (value) => {
76
97
  if (value === false)
@@ -107,7 +128,7 @@ const emit = ({ source, method, url, requestHeaders, responseStatus, duration, r
107
128
  }, sourceRule, normalizedSource);
108
129
  _storage
109
130
  .writeEntry({
110
- uuid: crypto.randomUUID(),
131
+ uuid: generateUuid(),
111
132
  batchId: TraceContext.getBatchId(),
112
133
  type: EntryType.CLIENT_REQUEST,
113
134
  content,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.4.96",
3
+ "version": "0.5.0",
4
4
  "description": "Trace assistant for ZinTrust: logs requests, queries, exceptions, jobs, and more.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  "node": ">=20.0.0"
41
41
  },
42
42
  "peerDependencies": {
43
- "@zintrust/core": "^0.4.95"
43
+ "@zintrust/core": "^0.4.101"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
package/src/config.ts CHANGED
@@ -134,7 +134,7 @@ const collectClientRequestSourceKeys = (
134
134
  ): string[] => {
135
135
  const overrideSources = override?.sources ?? {};
136
136
  const sourceKeys = new Set<string>([
137
- ...Object.keys(isObjectValue(base) ? base.sources ?? {} : {}),
137
+ ...Object.keys(isObjectValue(base) ? (base.sources ?? {}) : {}),
138
138
  ...Object.keys(overrideSources),
139
139
  ]);
140
140
 
@@ -3,7 +3,7 @@
3
3
  * No auth in this layer — caller mounts middleware as needed.
4
4
  */
5
5
  import type { IRequest, IResponse } from '@zintrust/core';
6
- import type { EntryTypeValue, ITraceStorage } from '../types';
6
+ import type { EntryTypeValue, ITraceEntry, ITraceStorage } from '../types';
7
7
 
8
8
  // ---------------------------------------------------------------------------
9
9
  // Storage holder (set once from routes.ts)
@@ -57,6 +57,128 @@ const getNumericQueryParam = (req: IRequest, key: string): number | undefined =>
57
57
  return undefined;
58
58
  };
59
59
 
60
+ const DEFAULT_PER_PAGE = 50;
61
+ const MAX_PER_PAGE = 100;
62
+ const DEFAULT_REQUEST_PER_PAGE = 25;
63
+ const MAX_REQUEST_PER_PAGE = 50;
64
+ const SUMMARY_TEXT_LIMIT = 280;
65
+ const SUMMARY_ARRAY_LIMIT = 10;
66
+
67
+ type CompactTraceEntry = ITraceEntry<Record<string, unknown>> & {
68
+ hasDetails: true;
69
+ contentBytes?: number;
70
+ };
71
+
72
+ const truncateText = (value: string, limit = SUMMARY_TEXT_LIMIT): string =>
73
+ value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 3))}...`;
74
+
75
+ const compactValue = (value: unknown): unknown => {
76
+ if (typeof value === 'string') {
77
+ return truncateText(value);
78
+ }
79
+
80
+ if (
81
+ typeof value === 'number' ||
82
+ typeof value === 'boolean' ||
83
+ value === null ||
84
+ value === undefined
85
+ ) {
86
+ return value;
87
+ }
88
+
89
+ if (Array.isArray(value)) {
90
+ return value.slice(0, SUMMARY_ARRAY_LIMIT).map((item) => {
91
+ if (typeof item === 'string') {
92
+ return truncateText(item);
93
+ }
94
+
95
+ if (typeof item === 'number' || typeof item === 'boolean' || item === null) {
96
+ return item;
97
+ }
98
+
99
+ return '[complex]';
100
+ });
101
+ }
102
+
103
+ return undefined;
104
+ };
105
+
106
+ const pickCompactContent = (content: unknown, keys: readonly string[]): Record<string, unknown> => {
107
+ if (typeof content !== 'object' || content === null || Array.isArray(content)) {
108
+ return {};
109
+ }
110
+
111
+ const source = content as Record<string, unknown>;
112
+ const compact: Record<string, unknown> = {};
113
+
114
+ for (const key of keys) {
115
+ const value = compactValue(source[key]);
116
+ if (value !== undefined) {
117
+ compact[key] = value;
118
+ }
119
+ }
120
+
121
+ return compact;
122
+ };
123
+
124
+ const COMPACT_ENTRY_KEYS: Record<EntryTypeValue, readonly string[]> = {
125
+ request: [
126
+ 'method',
127
+ 'uri',
128
+ 'responseStatus',
129
+ 'duration',
130
+ 'memory',
131
+ 'middleware',
132
+ 'hostname',
133
+ 'userId',
134
+ ],
135
+ query: ['connection', 'sql', 'time', 'duration', 'slow', 'hash', 'hostname'],
136
+ exception: ['class', 'file', 'line', 'message', 'occurrences', 'hostname', 'userId'],
137
+ log: ['level', 'message', 'hostname'],
138
+ job: ['status', 'connection', 'queue', 'name', 'tries', 'timeout', 'hostname'],
139
+ cache: ['operation', 'key', 'hit', 'store', 'payloadLogged', 'ttl', 'duration', 'hostname'],
140
+ schedule: ['name', 'expression', 'status', 'duration', 'hostname'],
141
+ mail: ['to', 'subject', 'template', 'hostname'],
142
+ auth: ['event', 'userId', 'hostname'],
143
+ event: ['name', 'listenerCount', 'hostname'],
144
+ model: ['action', 'model', 'id', 'hostname'],
145
+ notification: ['channels', 'notifiable', 'notification', 'message', 'hostname'],
146
+ redis: ['command', 'duration', 'hostname'],
147
+ gate: ['ability', 'result', 'userId', 'subject', 'hostname'],
148
+ middleware: ['name', 'event', 'duration', 'hostname'],
149
+ command: ['name', 'exitCode', 'duration', 'hostname'],
150
+ batch: ['name', 'total', 'processed', 'failed', 'status', 'hostname'],
151
+ dump: ['file', 'line', 'hostname'],
152
+ view: ['template', 'duration', 'hostname'],
153
+ client_request: ['source', 'method', 'url', 'responseStatus', 'error', 'duration', 'hostname'],
154
+ };
155
+
156
+ const compactEntryContent = (entry: ITraceEntry): Record<string, unknown> =>
157
+ pickCompactContent(entry.content, COMPACT_ENTRY_KEYS[entry.type]);
158
+
159
+ const estimateContentBytes = (content: unknown): number | undefined => {
160
+ try {
161
+ return new TextEncoder().encode(JSON.stringify(content)).length;
162
+ } catch {
163
+ return undefined;
164
+ }
165
+ };
166
+
167
+ const compactListEntry = (entry: ITraceEntry): CompactTraceEntry => ({
168
+ ...entry,
169
+ content: compactEntryContent(entry),
170
+ hasDetails: true,
171
+ contentBytes: estimateContentBytes(entry.content),
172
+ });
173
+
174
+ const resolvePerPage = (req: IRequest, type?: EntryTypeValue): number => {
175
+ const isRequestList = type === 'request';
176
+ const fallback = isRequestList ? DEFAULT_REQUEST_PER_PAGE : DEFAULT_PER_PAGE;
177
+ const limit = isRequestList ? MAX_REQUEST_PER_PAGE : MAX_PER_PAGE;
178
+
179
+ return Math.max(1, Math.min(qpInt(req, 'perPage', fallback), limit));
180
+ };
181
+
60
182
  // ---------------------------------------------------------------------------
61
183
  // Entry handlers
62
184
  // ---------------------------------------------------------------------------
@@ -64,18 +186,25 @@ const getNumericQueryParam = (req: IRequest, key: string): number | undefined =>
64
186
  export async function listEntries(req: IRequest, res: IResponse): Promise<void> {
65
187
  const storage = getStorage(res);
66
188
  if (storage !== null) {
189
+ const type = qp(req, 'type') as EntryTypeValue | undefined;
67
190
  const opts = {
68
- type: qp(req, 'type') as EntryTypeValue | undefined,
191
+ type,
69
192
  tag: qp(req, 'tag'),
70
193
  batchId: qp(req, 'batchId'),
71
194
  from: getNumericQueryParam(req, 'from'),
72
195
  to: getNumericQueryParam(req, 'to'),
73
- page: qpInt(req, 'page', 1),
74
- perPage: Math.min(qpInt(req, 'perPage', 50), 200),
196
+ page: Math.max(1, qpInt(req, 'page', 1)),
197
+ perPage: resolvePerPage(req, type),
75
198
  };
76
199
  try {
77
200
  const result = await storage.queryEntries(opts);
78
- res.json({ ok: true, ...result, page: opts.page, perPage: opts.perPage });
201
+ res.json({
202
+ ok: true,
203
+ data: result.data.map(compactListEntry),
204
+ total: result.total,
205
+ page: opts.page,
206
+ perPage: opts.perPage,
207
+ });
79
208
  } catch (err) {
80
209
  res.setStatus(500).json({ error: (err as Error).message });
81
210
  }
@@ -139,9 +139,6 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
139
139
  const MOON_ICON = __TRACE_MOON_ICON__;
140
140
  const COPY_ICON = __TRACE_COPY_ICON__;
141
141
  const DISCLOSURE_ICON = __TRACE_DISCLOSURE_ICON__;
142
- .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}
143
- .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)}
144
- .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)}
145
142
  const JSON_HIGHLIGHT_PATTERN = new RegExp(__TRACE_JSON_REGEX__, 'g');
146
143
  const SQL_HIGHLIGHT_PATTERN = new RegExp(__TRACE_SQL_REGEX__, 'gim');
147
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'];
@@ -1,13 +1,14 @@
1
+ import { generateUuid } from '@zintrust/core';
1
2
  import { TraceContext } from '../context';
2
- import type {
3
- ClientRequestContent,
4
- ClientRequestTraceInput,
5
- ITraceWatcher,
6
- ITraceWatcherConfig,
7
- TraceClientRequestCaptureRule,
8
- TraceClientRequestWatcherConfig,
3
+ import {
4
+ EntryType,
5
+ type ClientRequestContent,
6
+ type ClientRequestTraceInput,
7
+ type ITraceWatcher,
8
+ type ITraceWatcherConfig,
9
+ type TraceClientRequestCaptureRule,
10
+ type TraceClientRequestWatcherConfig,
9
11
  } from '../types';
10
- import { EntryType } from '../types';
11
12
  import { AuthTag } from '../utils/authTag';
12
13
  import { redactHeaders, redactUnknown } from '../utils/redact';
13
14
  import { RequestFilter } from '../utils/requestFilter';
@@ -85,24 +86,56 @@ const buildResponseBody = (
85
86
  return { responseBody: redactUnknown(responseBody, _redactBodyFields) };
86
87
  };
87
88
 
89
+ const applySource = (content: ClientRequestContent, normalizedSource: string | undefined): void => {
90
+ if (normalizedSource !== undefined) {
91
+ content.source = normalizedSource;
92
+ }
93
+ };
94
+
95
+ const applyResponseStatus = (
96
+ content: ClientRequestContent,
97
+ responseStatus: number | undefined
98
+ ): void => {
99
+ if (responseStatus !== undefined) {
100
+ content.responseStatus = responseStatus;
101
+ }
102
+ };
103
+
104
+ const applyError = (content: ClientRequestContent, error: unknown): void => {
105
+ if (typeof error === 'string' && error !== '') {
106
+ content.error = error;
107
+ }
108
+ };
109
+
110
+ const mergePartialContent = (
111
+ content: ClientRequestContent,
112
+ partial: Partial<ClientRequestContent>
113
+ ): void => {
114
+ Object.assign(content, partial);
115
+ };
116
+
88
117
  const buildClientRequestContent = (
89
118
  input: ClientRequestTraceInput,
90
119
  sourceRule: TraceClientRequestCaptureRule | undefined,
91
120
  normalizedSource: string | undefined
92
121
  ): ClientRequestContent => {
93
- return {
94
- ...(normalizedSource === undefined ? {} : { source: normalizedSource }),
122
+ const content: ClientRequestContent = {
95
123
  method: input.method.toUpperCase(),
96
124
  url: input.url,
97
- ...buildRequestHeaders(input.requestHeaders, sourceRule),
98
- ...buildRequestBody(input.requestBody, sourceRule),
99
- ...(input.responseStatus === undefined ? {} : { responseStatus: input.responseStatus }),
100
- ...buildResponseHeaders(input.responseHeaders, sourceRule),
101
- ...buildResponseBody(input.responseBody, sourceRule),
102
- ...(typeof input.error === 'string' && input.error !== '' ? { error: input.error } : {}),
125
+ requestHeaders: {},
103
126
  duration: input.duration,
104
127
  hostname: TraceContext.getHostname(),
105
128
  };
129
+
130
+ applySource(content, normalizedSource);
131
+ mergePartialContent(content, buildRequestHeaders(input.requestHeaders, sourceRule));
132
+ mergePartialContent(content, buildRequestBody(input.requestBody, sourceRule));
133
+ applyResponseStatus(content, input.responseStatus);
134
+ mergePartialContent(content, buildResponseHeaders(input.responseHeaders, sourceRule));
135
+ mergePartialContent(content, buildResponseBody(input.responseBody, sourceRule));
136
+ applyError(content, input.error);
137
+
138
+ return content;
106
139
  };
107
140
 
108
141
  const isWatcherEnabled = (
@@ -151,7 +184,7 @@ const emit = ({
151
184
  );
152
185
  _storage
153
186
  .writeEntry({
154
- uuid: crypto.randomUUID(),
187
+ uuid: generateUuid(),
155
188
  batchId: TraceContext.getBatchId(),
156
189
  type: EntryType.CLIENT_REQUEST,
157
190
  content,
@@ -169,7 +202,7 @@ export const HttpClientWatcher: ITraceWatcher & { emit: typeof emit } = Object.f
169
202
  _storage = storage;
170
203
  _clientRequestWatcher =
171
204
  typeof config.watchers.clientRequest === 'object' && config.watchers.clientRequest !== null
172
- ? (config.watchers.clientRequest as TraceClientRequestWatcherConfig)
205
+ ? config.watchers.clientRequest
173
206
  : undefined;
174
207
  _redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
175
208
  _redactBodyFields = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];