@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.
- package/dist/build-manifest.json +16 -16
- package/dist/config.js +1 -1
- package/dist/dashboard/handlers.js +107 -4
- package/dist/dashboard/ui.js +0 -3
- package/dist/watchers/HttpClientWatcher.d.ts +1 -1
- package/dist/watchers/HttpClientWatcher.js +31 -10
- package/package.json +2 -2
- package/src/config.ts +1 -1
- package/src/dashboard/handlers.ts +134 -5
- package/src/dashboard/ui.ts +0 -3
- package/src/watchers/HttpClientWatcher.ts +51 -18
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,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
|
|
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:
|
|
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({
|
|
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 });
|
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'];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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
|
-
|
|
62
|
-
...(normalizedSource === undefined ? {} : { source: normalizedSource }),
|
|
80
|
+
const content = {
|
|
63
81
|
method: input.method.toUpperCase(),
|
|
64
82
|
url: input.url,
|
|
65
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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({
|
|
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
|
}
|
package/src/dashboard/ui.ts
CHANGED
|
@@ -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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
94
|
-
...(normalizedSource === undefined ? {} : { source: normalizedSource }),
|
|
122
|
+
const content: ClientRequestContent = {
|
|
95
123
|
method: input.method.toUpperCase(),
|
|
96
124
|
url: input.url,
|
|
97
|
-
|
|
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:
|
|
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
|
-
?
|
|
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 ?? [])];
|