@zintrust/trace 0.4.95 → 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 +23 -23
- package/dist/config.js +83 -6
- package/dist/dashboard/handlers.js +107 -4
- package/dist/dashboard/ui.js +4 -0
- package/dist/storage/TraceContentBudget.js +67 -15
- package/dist/types.d.ts +13 -1
- package/dist/utils/entryFilter.js +20 -0
- package/dist/watchers/HttpClientWatcher.d.ts +2 -2
- package/dist/watchers/HttpClientWatcher.js +118 -20
- package/package.json +2 -2
- package/src/config.ts +136 -6
- package/src/dashboard/handlers.ts +134 -5
- package/src/dashboard/ui.ts +4 -0
- package/src/storage/TraceContentBudget.ts +98 -17
- package/src/types.ts +15 -1
- package/src/utils/entryFilter.ts +23 -0
- package/src/watchers/HttpClientWatcher.ts +165 -26
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": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": {
|
|
24
24
|
"build-manifest.json": {
|
|
25
|
-
"size":
|
|
26
|
-
"sha256": "
|
|
25
|
+
"size": 14739,
|
|
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,
|
|
@@ -150,8 +150,8 @@
|
|
|
150
150
|
"sha256": "d899a615e6cf2a5eea51f6200347cb81fbaae11979b801859f57135083aaf85b"
|
|
151
151
|
},
|
|
152
152
|
"storage/TraceContentBudget.js": {
|
|
153
|
-
"size":
|
|
154
|
-
"sha256": "
|
|
153
|
+
"size": 5868,
|
|
154
|
+
"sha256": "020597ed8a44def06677b8f0ae3947f44cb8c53d03803a2cf214a831858288a7"
|
|
155
155
|
},
|
|
156
156
|
"storage/TraceContentRedaction.d.ts": {
|
|
157
157
|
"size": 207,
|
|
@@ -194,8 +194,8 @@
|
|
|
194
194
|
"sha256": "d916e8e3abb1b1087f6b184851b0e6265e53380d7857b008e745d566aad15d44"
|
|
195
195
|
},
|
|
196
196
|
"types.d.ts": {
|
|
197
|
-
"size":
|
|
198
|
-
"sha256": "
|
|
197
|
+
"size": 8919,
|
|
198
|
+
"sha256": "4ff6c7c067fa9d16535e4356181a73325977c6813a13983ae9edb17c5fa3c466"
|
|
199
199
|
},
|
|
200
200
|
"types.js": {
|
|
201
201
|
"size": 696,
|
|
@@ -222,8 +222,8 @@
|
|
|
222
222
|
"sha256": "a7809e98f76b4e326262c26b0e43e278b1c50ac0b35fee145e3e6006357dc700"
|
|
223
223
|
},
|
|
224
224
|
"utils/entryFilter.js": {
|
|
225
|
-
"size":
|
|
226
|
-
"sha256": "
|
|
225
|
+
"size": 4123,
|
|
226
|
+
"sha256": "de2690c6e5852969083387b469fd2e12f10470c6eee81120154437cd8d8f3555"
|
|
227
227
|
},
|
|
228
228
|
"utils/familyHash.d.ts": {
|
|
229
229
|
"size": 60,
|
|
@@ -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
|
@@ -12,15 +12,26 @@ const mergeStringLists = (base, override) => {
|
|
|
12
12
|
const isObjectValue = (value) => {
|
|
13
13
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
14
|
};
|
|
15
|
+
const resolveEnabled = (base, override) => {
|
|
16
|
+
return override?.enabled ?? base?.enabled;
|
|
17
|
+
};
|
|
18
|
+
const hasMergedRuleValues = (include, exclude, enabled) => {
|
|
19
|
+
return include.length > 0 || exclude.length > 0 || enabled !== undefined;
|
|
20
|
+
};
|
|
21
|
+
const buildFilterRule = (input) => {
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
...(input.enabled === undefined ? {} : { enabled: input.enabled }),
|
|
24
|
+
...(input.include.length > 0 ? { include: input.include } : {}),
|
|
25
|
+
...(input.exclude.length > 0 ? { exclude: input.exclude } : {}),
|
|
26
|
+
});
|
|
27
|
+
};
|
|
15
28
|
const mergeFilterRule = (base, override) => {
|
|
16
29
|
const include = mergeStringLists(base?.include ?? [], override?.include);
|
|
17
30
|
const exclude = mergeStringLists(base?.exclude ?? [], override?.exclude);
|
|
18
|
-
|
|
31
|
+
const enabled = resolveEnabled(base, override);
|
|
32
|
+
if (!hasMergedRuleValues(include, exclude, enabled))
|
|
19
33
|
return undefined;
|
|
20
|
-
return
|
|
21
|
-
...(include.length > 0 ? { include } : {}),
|
|
22
|
-
...(exclude.length > 0 ? { exclude } : {}),
|
|
23
|
-
});
|
|
34
|
+
return buildFilterRule({ include, exclude, enabled });
|
|
24
35
|
};
|
|
25
36
|
const mergeWatcherToggle = (base, override) => {
|
|
26
37
|
if (override === undefined)
|
|
@@ -30,6 +41,72 @@ const mergeWatcherToggle = (base, override) => {
|
|
|
30
41
|
const baseRule = isObjectValue(base) ? base : undefined;
|
|
31
42
|
return mergeFilterRule(baseRule, override);
|
|
32
43
|
};
|
|
44
|
+
const resolveClientRequestCaptureFlags = (base, override) => {
|
|
45
|
+
return {
|
|
46
|
+
requestHeaders: override?.requestHeaders ?? base?.requestHeaders,
|
|
47
|
+
requestBody: override?.requestBody ?? base?.requestBody,
|
|
48
|
+
responseHeaders: override?.responseHeaders ?? base?.responseHeaders,
|
|
49
|
+
responseBody: override?.responseBody ?? base?.responseBody,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
const hasClientRequestCaptureFlags = (flags) => {
|
|
53
|
+
return Object.values(flags).some((value) => value !== undefined);
|
|
54
|
+
};
|
|
55
|
+
const buildClientRequestCaptureRule = (mergedRule, flags) => {
|
|
56
|
+
const baseRule = mergedRule ? { ...mergedRule } : {};
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
...baseRule,
|
|
59
|
+
...(flags.requestHeaders === undefined ? {} : { requestHeaders: flags.requestHeaders }),
|
|
60
|
+
...(flags.requestBody === undefined ? {} : { requestBody: flags.requestBody }),
|
|
61
|
+
...(flags.responseHeaders === undefined ? {} : { responseHeaders: flags.responseHeaders }),
|
|
62
|
+
...(flags.responseBody === undefined ? {} : { responseBody: flags.responseBody }),
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
const mergeClientRequestCaptureRule = (base, override) => {
|
|
66
|
+
const mergedRule = mergeFilterRule(base, override);
|
|
67
|
+
const flags = resolveClientRequestCaptureFlags(base, override);
|
|
68
|
+
if (mergedRule === undefined && !hasClientRequestCaptureFlags(flags)) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return buildClientRequestCaptureRule(mergedRule, flags);
|
|
72
|
+
};
|
|
73
|
+
const collectClientRequestSourceKeys = (base, override) => {
|
|
74
|
+
const overrideSources = override?.sources ?? {};
|
|
75
|
+
const sourceKeys = new Set([
|
|
76
|
+
...Object.keys(isObjectValue(base) ? (base.sources ?? {}) : {}),
|
|
77
|
+
...Object.keys(overrideSources),
|
|
78
|
+
]);
|
|
79
|
+
return [...sourceKeys];
|
|
80
|
+
};
|
|
81
|
+
const mergeClientRequestSources = (base, override) => {
|
|
82
|
+
if (override === undefined)
|
|
83
|
+
return undefined;
|
|
84
|
+
const sources = {};
|
|
85
|
+
for (const key of collectClientRequestSourceKeys(base, override)) {
|
|
86
|
+
const baseSources = isObjectValue(base) ? base.sources : undefined;
|
|
87
|
+
const sourceRule = mergeClientRequestCaptureRule(baseSources?.[key], override.sources?.[key]);
|
|
88
|
+
if (sourceRule !== undefined) {
|
|
89
|
+
sources[key] = sourceRule;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return Object.keys(sources).length === 0 ? undefined : sources;
|
|
93
|
+
};
|
|
94
|
+
const mergeClientRequestWatcherToggle = (base, override) => {
|
|
95
|
+
if (override === undefined)
|
|
96
|
+
return base;
|
|
97
|
+
if (override === false || override === true)
|
|
98
|
+
return override;
|
|
99
|
+
const baseConfig = isObjectValue(base) ? base : undefined;
|
|
100
|
+
const merged = mergeClientRequestCaptureRule(baseConfig, override) ?? {};
|
|
101
|
+
const sources = mergeClientRequestSources(base, override);
|
|
102
|
+
if (sources === undefined) {
|
|
103
|
+
return merged;
|
|
104
|
+
}
|
|
105
|
+
return Object.freeze({
|
|
106
|
+
...merged,
|
|
107
|
+
sources,
|
|
108
|
+
});
|
|
109
|
+
};
|
|
33
110
|
const REQUEST_METHOD_KEYS = ['all', 'get', 'post', 'put', 'patch', 'delete'];
|
|
34
111
|
const mergeRequestWatcherToggle = (base, override) => {
|
|
35
112
|
if (override === undefined)
|
|
@@ -70,7 +147,7 @@ const mergeWatchers = (base, override) => {
|
|
|
70
147
|
batch: mergeWatcherToggle(base.batch, override.batch),
|
|
71
148
|
dump: mergeWatcherToggle(base.dump, override.dump),
|
|
72
149
|
view: mergeWatcherToggle(base.view, override.view),
|
|
73
|
-
clientRequest:
|
|
150
|
+
clientRequest: mergeClientRequestWatcherToggle(base.clientRequest, override.clientRequest),
|
|
74
151
|
};
|
|
75
152
|
};
|
|
76
153
|
const DEFAULTS = Object.freeze({
|
|
@@ -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
|
@@ -35,6 +35,7 @@ const BRAND_SVG = `<svg width="120" height="120" viewBox="0 0 100 100" fill="non
|
|
|
35
35
|
const SUN_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2.2M12 19.8V22M4.93 4.93l1.56 1.56M17.51 17.51l1.56 1.56M2 12h2.2M19.8 12H22M4.93 19.07l1.56-1.56M17.51 6.49l1.56-1.56"></path></svg>`;
|
|
36
36
|
const MOON_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"></path></svg>`;
|
|
37
37
|
const COPY_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
|
|
38
|
+
const DISCLOSURE_ICON = `<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4l6 6-6 6"></path></svg>`;
|
|
38
39
|
const JSON_HIGHLIGHT_PATTERN = String.raw `("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")(?=\s*:)|(\s*:)|("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?`;
|
|
39
40
|
const SQL_HIGHLIGHT_PATTERN = String.raw `(\/\*[\s\S]*?\*\/|--.*$|'(?:''|[^'])*'|\x60[^\x60]+\x60|\b(?:select|from|where|insert|into|values|update|delete|join|left|right|inner|outer|on|and|or|limit|order|by|group|having|as|distinct|null|is|in|like|set|case|when|then|else|end|returning|union|all)\b|-?\d+(?:\.\d+)?)`;
|
|
40
41
|
const encodeSvgDataUri = (svg) => {
|
|
@@ -131,6 +132,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
131
132
|
const SUN_ICON = __TRACE_SUN_ICON__;
|
|
132
133
|
const MOON_ICON = __TRACE_MOON_ICON__;
|
|
133
134
|
const COPY_ICON = __TRACE_COPY_ICON__;
|
|
135
|
+
const DISCLOSURE_ICON = __TRACE_DISCLOSURE_ICON__;
|
|
134
136
|
const JSON_HIGHLIGHT_PATTERN = new RegExp(__TRACE_JSON_REGEX__, 'g');
|
|
135
137
|
const SQL_HIGHLIGHT_PATTERN = new RegExp(__TRACE_SQL_REGEX__, 'gim');
|
|
136
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'];
|
|
@@ -645,6 +647,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
645
647
|
return [
|
|
646
648
|
'<details class="trace-item trace-disclosure"' + (isInitiallyOpen ? ' open' : '') + '>',
|
|
647
649
|
'<summary class="trace-item-head trace-summary">',
|
|
650
|
+
'<span class="trace-summary-icon">' + DISCLOSURE_ICON + '</span>',
|
|
648
651
|
'<span class="trace-summary-main">',
|
|
649
652
|
'<span><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></span>',
|
|
650
653
|
'<span class="trace-summary-copy">' + entrySummaryInlineHtml(entry) + '</span>',
|
|
@@ -1075,6 +1078,7 @@ const buildDashboardHtml = (basePath, projectName) => {
|
|
|
1075
1078
|
.replace('__TRACE_SUN_ICON__', JSON.stringify(SUN_ICON))
|
|
1076
1079
|
.replace('__TRACE_MOON_ICON__', JSON.stringify(MOON_ICON))
|
|
1077
1080
|
.replace('__TRACE_COPY_ICON__', JSON.stringify(COPY_ICON))
|
|
1081
|
+
.replace('__TRACE_DISCLOSURE_ICON__', JSON.stringify(DISCLOSURE_ICON))
|
|
1078
1082
|
.replace('__TRACE_JSON_REGEX__', JSON.stringify(JSON_HIGHLIGHT_PATTERN))
|
|
1079
1083
|
.replace('__TRACE_SQL_REGEX__', JSON.stringify(SQL_HIGHLIGHT_PATTERN))
|
|
1080
1084
|
.replace('__TRACE_BASE_PATH_LABEL__', basePath)
|
|
@@ -21,6 +21,58 @@ const describeValueType = (value) => {
|
|
|
21
21
|
return 'null';
|
|
22
22
|
return typeof value;
|
|
23
23
|
};
|
|
24
|
+
const chooseLargerCandidate = (left, right) => {
|
|
25
|
+
if (left === null)
|
|
26
|
+
return right;
|
|
27
|
+
if (right === null)
|
|
28
|
+
return left;
|
|
29
|
+
return right.size > left.size ? right : left;
|
|
30
|
+
};
|
|
31
|
+
const fallbackCandidate = (value, path) => {
|
|
32
|
+
return path.length === 0 ? null : { path, size: serializedSize(value) };
|
|
33
|
+
};
|
|
34
|
+
const findLargestDroppablePathInArray = (value, path) => {
|
|
35
|
+
let best = null;
|
|
36
|
+
for (const [index, item] of value.entries()) {
|
|
37
|
+
best = chooseLargerCandidate(best, findLargestDroppablePath(item, [...path, index]));
|
|
38
|
+
}
|
|
39
|
+
return best ?? fallbackCandidate(value, path);
|
|
40
|
+
};
|
|
41
|
+
const findLargestDroppablePathInObject = (value, path) => {
|
|
42
|
+
let best = null;
|
|
43
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
44
|
+
if (key === '__traceNotice')
|
|
45
|
+
continue;
|
|
46
|
+
best = chooseLargerCandidate(best, findLargestDroppablePath(entryValue, [...path, key]));
|
|
47
|
+
}
|
|
48
|
+
return best ?? fallbackCandidate(value, path);
|
|
49
|
+
};
|
|
50
|
+
const findLargestDroppablePath = (value, path = []) => {
|
|
51
|
+
if (Array.isArray(value))
|
|
52
|
+
return findLargestDroppablePathInArray(value, path);
|
|
53
|
+
if (typeof value === 'object' && value !== null) {
|
|
54
|
+
return findLargestDroppablePathInObject(value, path);
|
|
55
|
+
}
|
|
56
|
+
return fallbackCandidate(value, path);
|
|
57
|
+
};
|
|
58
|
+
const replaceAtPath = (value, path, replacement) => {
|
|
59
|
+
if (path.length === 0)
|
|
60
|
+
return replacement;
|
|
61
|
+
const [segment, ...rest] = path;
|
|
62
|
+
if (Array.isArray(value) && typeof segment === 'number') {
|
|
63
|
+
const next = value.slice();
|
|
64
|
+
next[segment] = replaceAtPath(next[segment], rest, replacement);
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
if (typeof value === 'object' && value !== null && typeof segment === 'string') {
|
|
68
|
+
const current = value;
|
|
69
|
+
return {
|
|
70
|
+
...current,
|
|
71
|
+
[segment]: replaceAtPath(current[segment], rest, replacement),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
};
|
|
24
76
|
const compactValue = (value, depth) => {
|
|
25
77
|
if (depth >= DEFAULT_MAX_DEPTH) {
|
|
26
78
|
return DROPPED_FIELD_MESSAGE;
|
|
@@ -52,18 +104,18 @@ const compactValue = (value, depth) => {
|
|
|
52
104
|
}
|
|
53
105
|
return Object.fromEntries(compactedEntries);
|
|
54
106
|
};
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
107
|
+
const compactStructuredValueToBudget = (value) => {
|
|
108
|
+
let compacted = typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
109
|
+
? {
|
|
110
|
+
...value,
|
|
111
|
+
__traceNotice: COMPACTED_CONTENT_MESSAGE,
|
|
112
|
+
}
|
|
113
|
+
: value;
|
|
114
|
+
while (serializedSize(compacted) > DEFAULT_MAX_ENTRY_BYTES) {
|
|
115
|
+
const candidate = findLargestDroppablePath(compacted);
|
|
116
|
+
if (candidate === null)
|
|
65
117
|
break;
|
|
66
|
-
compacted
|
|
118
|
+
compacted = replaceAtPath(compacted, candidate.path, DROPPED_FIELD_MESSAGE);
|
|
67
119
|
}
|
|
68
120
|
return compacted;
|
|
69
121
|
};
|
|
@@ -75,10 +127,10 @@ const fitContentToBudget = (content) => {
|
|
|
75
127
|
if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
76
128
|
return compacted;
|
|
77
129
|
}
|
|
78
|
-
if (typeof compacted === 'object' && compacted !== null
|
|
79
|
-
const
|
|
80
|
-
if (serializedSize(
|
|
81
|
-
return
|
|
130
|
+
if (typeof compacted === 'object' && compacted !== null) {
|
|
131
|
+
const budgetCompacted = compactStructuredValueToBudget(compacted);
|
|
132
|
+
if (serializedSize(budgetCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
133
|
+
return budgetCompacted;
|
|
82
134
|
}
|
|
83
135
|
}
|
|
84
136
|
return {
|
package/dist/types.d.ts
CHANGED
|
@@ -189,6 +189,7 @@ export interface ViewContent {
|
|
|
189
189
|
hostname: string;
|
|
190
190
|
}
|
|
191
191
|
export interface ClientRequestContent {
|
|
192
|
+
source?: string;
|
|
192
193
|
method: string;
|
|
193
194
|
url: string;
|
|
194
195
|
requestHeaders: Record<string, string>;
|
|
@@ -201,6 +202,7 @@ export interface ClientRequestContent {
|
|
|
201
202
|
hostname: string;
|
|
202
203
|
}
|
|
203
204
|
export interface ClientRequestTraceInput {
|
|
205
|
+
source?: string;
|
|
204
206
|
method: string;
|
|
205
207
|
url: string;
|
|
206
208
|
requestHeaders: Record<string, string>;
|
|
@@ -268,6 +270,12 @@ export type TraceFilterRule = {
|
|
|
268
270
|
include?: string[];
|
|
269
271
|
exclude?: string[];
|
|
270
272
|
};
|
|
273
|
+
export type TraceClientRequestCaptureRule = TraceFilterRule & {
|
|
274
|
+
requestHeaders?: boolean;
|
|
275
|
+
requestBody?: boolean;
|
|
276
|
+
responseHeaders?: boolean;
|
|
277
|
+
responseBody?: boolean;
|
|
278
|
+
};
|
|
271
279
|
export type TraceRequestWatcherConfig = TraceFilterRule & {
|
|
272
280
|
all?: TraceFilterRule;
|
|
273
281
|
get?: TraceFilterRule;
|
|
@@ -276,8 +284,12 @@ export type TraceRequestWatcherConfig = TraceFilterRule & {
|
|
|
276
284
|
patch?: TraceFilterRule;
|
|
277
285
|
delete?: TraceFilterRule;
|
|
278
286
|
};
|
|
287
|
+
export type TraceClientRequestWatcherConfig = TraceClientRequestCaptureRule & {
|
|
288
|
+
sources?: Record<string, TraceClientRequestCaptureRule>;
|
|
289
|
+
};
|
|
279
290
|
export type TraceWatcherToggle = boolean | TraceFilterRule;
|
|
280
291
|
export type TraceRequestWatcherToggle = boolean | TraceRequestWatcherConfig;
|
|
292
|
+
export type TraceClientRequestWatcherToggle = boolean | TraceClientRequestWatcherConfig;
|
|
281
293
|
export type WatcherToggles = {
|
|
282
294
|
request?: TraceRequestWatcherToggle;
|
|
283
295
|
query?: TraceWatcherToggle;
|
|
@@ -298,7 +310,7 @@ export type WatcherToggles = {
|
|
|
298
310
|
batch?: TraceWatcherToggle;
|
|
299
311
|
dump?: TraceWatcherToggle;
|
|
300
312
|
view?: TraceWatcherToggle;
|
|
301
|
-
clientRequest?:
|
|
313
|
+
clientRequest?: TraceClientRequestWatcherToggle;
|
|
302
314
|
};
|
|
303
315
|
export interface ITraceConfig {
|
|
304
316
|
enabled: boolean;
|
|
@@ -13,6 +13,8 @@ const normalizeTerms = (terms) => {
|
|
|
13
13
|
const matchesRule = (haystack, rule) => {
|
|
14
14
|
if (!rule)
|
|
15
15
|
return true;
|
|
16
|
+
if (rule.enabled === false)
|
|
17
|
+
return false;
|
|
16
18
|
const include = normalizeTerms(rule.include);
|
|
17
19
|
const exclude = normalizeTerms(rule.exclude);
|
|
18
20
|
if (exclude.some((term) => haystack.includes(term)))
|
|
@@ -71,6 +73,16 @@ const getRequestMethodRule = (watcher, entry) => {
|
|
|
71
73
|
return watcher.delete;
|
|
72
74
|
return watcher.all;
|
|
73
75
|
};
|
|
76
|
+
const getClientRequestSourceRule = (watcher, entry) => {
|
|
77
|
+
if (entry.type !== EntryType.CLIENT_REQUEST)
|
|
78
|
+
return undefined;
|
|
79
|
+
const content = isObjectValue(entry.content) ? entry.content : undefined;
|
|
80
|
+
const sourceValue = content?.['source'];
|
|
81
|
+
const source = typeof sourceValue === 'string' ? sourceValue.trim().toLowerCase() : '';
|
|
82
|
+
if (source === '')
|
|
83
|
+
return undefined;
|
|
84
|
+
return watcher.sources?.[source];
|
|
85
|
+
};
|
|
74
86
|
export const TraceEntryFilter = Object.freeze({
|
|
75
87
|
shouldCapture(entry, config) {
|
|
76
88
|
const watcherKey = watcherKeyByEntryType[entry.type];
|
|
@@ -90,6 +102,14 @@ export const TraceEntryFilter = Object.freeze({
|
|
|
90
102
|
if (!matchesRule(haystack, methodRule))
|
|
91
103
|
return false;
|
|
92
104
|
}
|
|
105
|
+
if (watcherKey === 'clientRequest') {
|
|
106
|
+
const clientRequestWatcher = watcher;
|
|
107
|
+
const sourceRule = getClientRequestSourceRule(clientRequestWatcher, entry);
|
|
108
|
+
if (sourceRule?.enabled === false)
|
|
109
|
+
return false;
|
|
110
|
+
if (!matchesRule(haystack, sourceRule))
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
93
113
|
return true;
|
|
94
114
|
},
|
|
95
115
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
declare const emit: ({ method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }: ClientRequestTraceInput) => void;
|
|
1
|
+
import { type ClientRequestTraceInput, type ITraceWatcher } from '../types';
|
|
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;
|
|
5
5
|
};
|