@zintrust/trace 0.4.95 → 0.4.96
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 +20 -20
- package/dist/config.js +83 -6
- package/dist/dashboard/ui.js +7 -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 +1 -1
- package/dist/watchers/HttpClientWatcher.js +95 -18
- package/package.json +2 -2
- package/src/config.ts +136 -6
- package/src/dashboard/ui.ts +7 -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 +125 -19
package/dist/build-manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.4.96",
|
|
4
|
+
"buildDate": "2026-04-11T20:49:58.121Z",
|
|
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": "ddf9b233",
|
|
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": "b57ad8dd90ba688df31448cae52218fcfc3be48d533c55a06db0247741f30c8d"
|
|
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": 9270,
|
|
42
|
+
"sha256": "346361028d355391f3068c340db559dce558145e173bf248aa8d1c6328575de3"
|
|
43
43
|
},
|
|
44
44
|
"context.d.ts": {
|
|
45
45
|
"size": 596,
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
"sha256": "4862b41e0477f01afa0dbb446d4553b65c22ed774cd1e2db3489059ced392f94"
|
|
71
71
|
},
|
|
72
72
|
"dashboard/ui.js": {
|
|
73
|
-
"size":
|
|
74
|
-
"sha256": "
|
|
73
|
+
"size": 79698,
|
|
74
|
+
"sha256": "60295315a1fbbfd02018a971399739971252ff16f252ddfea0ea43a562aa305f"
|
|
75
75
|
},
|
|
76
76
|
"index.d.ts": {
|
|
77
77
|
"size": 2537,
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"index.js": {
|
|
81
81
|
"size": 3325,
|
|
82
|
-
"sha256": "
|
|
82
|
+
"sha256": "fc1e557217d22f31fa29611bebd54d9a0d93393f9a425283f238d91a30b216c9"
|
|
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": 341,
|
|
326
|
+
"sha256": "7e20bd9240de2165def5d90ec529e4da4e0a7302bc855bd2fb873f8b71d0182f"
|
|
327
327
|
},
|
|
328
328
|
"watchers/HttpClientWatcher.js": {
|
|
329
|
-
"size":
|
|
330
|
-
"sha256": "
|
|
329
|
+
"size": 5245,
|
|
330
|
+
"sha256": "dcbc10ac6fd583a009bf37c6787738765f5b54bd2a15a1cd6582d0bb2bbb1207"
|
|
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({
|
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,10 @@ 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__;
|
|
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)}
|
|
134
139
|
const JSON_HIGHLIGHT_PATTERN = new RegExp(__TRACE_JSON_REGEX__, 'g');
|
|
135
140
|
const SQL_HIGHLIGHT_PATTERN = new RegExp(__TRACE_SQL_REGEX__, 'gim');
|
|
136
141
|
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 +650,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
|
|
|
645
650
|
return [
|
|
646
651
|
'<details class="trace-item trace-disclosure"' + (isInitiallyOpen ? ' open' : '') + '>',
|
|
647
652
|
'<summary class="trace-item-head trace-summary">',
|
|
653
|
+
'<span class="trace-summary-icon">' + DISCLOSURE_ICON + '</span>',
|
|
648
654
|
'<span class="trace-summary-main">',
|
|
649
655
|
'<span><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></span>',
|
|
650
656
|
'<span class="trace-summary-copy">' + entrySummaryInlineHtml(entry) + '</span>',
|
|
@@ -1075,6 +1081,7 @@ const buildDashboardHtml = (basePath, projectName) => {
|
|
|
1075
1081
|
.replace('__TRACE_SUN_ICON__', JSON.stringify(SUN_ICON))
|
|
1076
1082
|
.replace('__TRACE_MOON_ICON__', JSON.stringify(MOON_ICON))
|
|
1077
1083
|
.replace('__TRACE_COPY_ICON__', JSON.stringify(COPY_ICON))
|
|
1084
|
+
.replace('__TRACE_DISCLOSURE_ICON__', JSON.stringify(DISCLOSURE_ICON))
|
|
1078
1085
|
.replace('__TRACE_JSON_REGEX__', JSON.stringify(JSON_HIGHLIGHT_PATTERN))
|
|
1079
1086
|
.replace('__TRACE_SQL_REGEX__', JSON.stringify(SQL_HIGHLIGHT_PATTERN))
|
|
1080
1087
|
.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
1
|
import type { ClientRequestTraceInput, ITraceWatcher } from '../types';
|
|
2
|
-
declare const emit: ({ method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }: ClientRequestTraceInput) => void;
|
|
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
|
};
|
|
@@ -7,32 +7,104 @@ let _storage = null;
|
|
|
7
7
|
let _redactHeaderNames = [];
|
|
8
8
|
let _redactBodyFields = [];
|
|
9
9
|
let _ignoreRoutes = [];
|
|
10
|
-
|
|
10
|
+
let _clientRequestWatcher;
|
|
11
|
+
const isObjectValue = (value) => {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
};
|
|
14
|
+
const resolveSource = (value) => {
|
|
15
|
+
if (typeof value !== 'string')
|
|
16
|
+
return undefined;
|
|
17
|
+
const normalized = value.trim().toLowerCase();
|
|
18
|
+
return normalized === '' ? undefined : normalized;
|
|
19
|
+
};
|
|
20
|
+
const resolveSourceRule = (source) => {
|
|
21
|
+
if (source === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
return _clientRequestWatcher?.sources?.[source];
|
|
24
|
+
};
|
|
25
|
+
const shouldCaptureField = (field, sourceRule) => {
|
|
26
|
+
const scoped = sourceRule?.[field];
|
|
27
|
+
if (typeof scoped === 'boolean')
|
|
28
|
+
return scoped;
|
|
29
|
+
const global = _clientRequestWatcher?.[field];
|
|
30
|
+
if (typeof global === 'boolean')
|
|
31
|
+
return global;
|
|
32
|
+
return true;
|
|
33
|
+
};
|
|
34
|
+
const buildRequestHeaders = (requestHeaders, sourceRule) => {
|
|
35
|
+
return shouldCaptureField('requestHeaders', sourceRule)
|
|
36
|
+
? { requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames) }
|
|
37
|
+
: { requestHeaders: {} };
|
|
38
|
+
};
|
|
39
|
+
const buildRequestBody = (requestBody, sourceRule) => {
|
|
40
|
+
if (requestBody === undefined)
|
|
41
|
+
return {};
|
|
42
|
+
if (!shouldCaptureField('requestBody', sourceRule))
|
|
43
|
+
return {};
|
|
44
|
+
return { requestBody: redactUnknown(requestBody, _redactBodyFields) };
|
|
45
|
+
};
|
|
46
|
+
const buildResponseHeaders = (responseHeaders, sourceRule) => {
|
|
47
|
+
if (responseHeaders === undefined)
|
|
48
|
+
return {};
|
|
49
|
+
if (!shouldCaptureField('responseHeaders', sourceRule))
|
|
50
|
+
return {};
|
|
51
|
+
return { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) };
|
|
52
|
+
};
|
|
53
|
+
const buildResponseBody = (responseBody, sourceRule) => {
|
|
54
|
+
if (responseBody === undefined)
|
|
55
|
+
return {};
|
|
56
|
+
if (!shouldCaptureField('responseBody', sourceRule))
|
|
57
|
+
return {};
|
|
58
|
+
return { responseBody: redactUnknown(responseBody, _redactBodyFields) };
|
|
59
|
+
};
|
|
60
|
+
const buildClientRequestContent = (input, sourceRule, normalizedSource) => {
|
|
61
|
+
return {
|
|
62
|
+
...(normalizedSource === undefined ? {} : { source: normalizedSource }),
|
|
63
|
+
method: input.method.toUpperCase(),
|
|
64
|
+
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 } : {}),
|
|
71
|
+
duration: input.duration,
|
|
72
|
+
hostname: TraceContext.getHostname(),
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const isWatcherEnabled = (value) => {
|
|
76
|
+
if (value === false)
|
|
77
|
+
return false;
|
|
78
|
+
if (isObjectValue(value) && value.enabled === false)
|
|
79
|
+
return false;
|
|
80
|
+
return true;
|
|
81
|
+
};
|
|
82
|
+
const emit = ({ source, method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }) => {
|
|
11
83
|
if (!_storage)
|
|
12
84
|
return;
|
|
13
85
|
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes))
|
|
14
86
|
return;
|
|
87
|
+
const normalizedSource = resolveSource(source);
|
|
88
|
+
const sourceRule = resolveSourceRule(normalizedSource);
|
|
89
|
+
if (sourceRule?.enabled === false)
|
|
90
|
+
return;
|
|
15
91
|
const tags = AuthTag.append([method.toUpperCase()]);
|
|
16
92
|
if ((responseStatus ?? 0) >= 400 || error)
|
|
17
93
|
tags.push('failed');
|
|
18
|
-
|
|
19
|
-
|
|
94
|
+
if (normalizedSource !== undefined)
|
|
95
|
+
tags.push(normalizedSource);
|
|
96
|
+
const content = buildClientRequestContent({
|
|
97
|
+
source,
|
|
98
|
+
method,
|
|
20
99
|
url,
|
|
21
|
-
requestHeaders
|
|
22
|
-
|
|
23
|
-
? {}
|
|
24
|
-
: { requestBody: redactUnknown(requestBody, _redactBodyFields) }),
|
|
25
|
-
...(responseStatus === undefined ? {} : { responseStatus }),
|
|
26
|
-
...(responseHeaders === undefined
|
|
27
|
-
? {}
|
|
28
|
-
: { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) }),
|
|
29
|
-
...(responseBody === undefined
|
|
30
|
-
? {}
|
|
31
|
-
: { responseBody: redactUnknown(responseBody, _redactBodyFields) }),
|
|
32
|
-
...(typeof error === 'string' && error !== '' ? { error } : {}),
|
|
100
|
+
requestHeaders,
|
|
101
|
+
responseStatus,
|
|
33
102
|
duration,
|
|
34
|
-
|
|
35
|
-
|
|
103
|
+
requestBody,
|
|
104
|
+
responseHeaders,
|
|
105
|
+
responseBody,
|
|
106
|
+
error,
|
|
107
|
+
}, sourceRule, normalizedSource);
|
|
36
108
|
_storage
|
|
37
109
|
.writeEntry({
|
|
38
110
|
uuid: crypto.randomUUID(),
|
|
@@ -48,14 +120,19 @@ const emit = ({ method, url, requestHeaders, responseStatus, duration, requestBo
|
|
|
48
120
|
export const HttpClientWatcher = Object.freeze({
|
|
49
121
|
emit,
|
|
50
122
|
register({ storage, config }) {
|
|
51
|
-
if (config.watchers.clientRequest
|
|
123
|
+
if (!isWatcherEnabled(config.watchers.clientRequest))
|
|
52
124
|
return () => undefined;
|
|
53
125
|
_storage = storage;
|
|
126
|
+
_clientRequestWatcher =
|
|
127
|
+
typeof config.watchers.clientRequest === 'object' && config.watchers.clientRequest !== null
|
|
128
|
+
? config.watchers.clientRequest
|
|
129
|
+
: undefined;
|
|
54
130
|
_redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
|
|
55
131
|
_redactBodyFields = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
|
|
56
132
|
_ignoreRoutes = config.ignoreRoutes;
|
|
57
133
|
return () => {
|
|
58
134
|
_storage = null;
|
|
135
|
+
_clientRequestWatcher = undefined;
|
|
59
136
|
_redactBodyFields = [];
|
|
60
137
|
_ignoreRoutes = [];
|
|
61
138
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.96",
|
|
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.95"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
package/src/config.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type {
|
|
5
5
|
ITraceConfig,
|
|
6
|
+
TraceClientRequestCaptureRule,
|
|
7
|
+
TraceClientRequestWatcherToggle,
|
|
6
8
|
TraceConfigOverrides,
|
|
7
9
|
TraceFilterRule,
|
|
8
10
|
TraceRequestWatcherConfig,
|
|
@@ -25,19 +27,44 @@ const isObjectValue = (value: unknown): value is Record<string, unknown> => {
|
|
|
25
27
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
const resolveEnabled = (
|
|
31
|
+
base?: TraceFilterRule,
|
|
32
|
+
override?: TraceFilterRule
|
|
33
|
+
): boolean | undefined => {
|
|
34
|
+
return override?.enabled ?? base?.enabled;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const hasMergedRuleValues = (
|
|
38
|
+
include: string[],
|
|
39
|
+
exclude: string[],
|
|
40
|
+
enabled: boolean | undefined
|
|
41
|
+
): boolean => {
|
|
42
|
+
return include.length > 0 || exclude.length > 0 || enabled !== undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const buildFilterRule = (input: {
|
|
46
|
+
include: string[];
|
|
47
|
+
exclude: string[];
|
|
48
|
+
enabled: boolean | undefined;
|
|
49
|
+
}): TraceFilterRule => {
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
...(input.enabled === undefined ? {} : { enabled: input.enabled }),
|
|
52
|
+
...(input.include.length > 0 ? { include: input.include } : {}),
|
|
53
|
+
...(input.exclude.length > 0 ? { exclude: input.exclude } : {}),
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
28
57
|
const mergeFilterRule = (
|
|
29
58
|
base?: TraceFilterRule,
|
|
30
59
|
override?: TraceFilterRule
|
|
31
60
|
): TraceFilterRule | undefined => {
|
|
32
61
|
const include = mergeStringLists(base?.include ?? [], override?.include);
|
|
33
62
|
const exclude = mergeStringLists(base?.exclude ?? [], override?.exclude);
|
|
63
|
+
const enabled = resolveEnabled(base, override);
|
|
34
64
|
|
|
35
|
-
if (include
|
|
65
|
+
if (!hasMergedRuleValues(include, exclude, enabled)) return undefined;
|
|
36
66
|
|
|
37
|
-
return
|
|
38
|
-
...(include.length > 0 ? { include } : {}),
|
|
39
|
-
...(exclude.length > 0 ? { exclude } : {}),
|
|
40
|
-
});
|
|
67
|
+
return buildFilterRule({ include, exclude, enabled });
|
|
41
68
|
};
|
|
42
69
|
|
|
43
70
|
const mergeWatcherToggle = (
|
|
@@ -51,6 +78,109 @@ const mergeWatcherToggle = (
|
|
|
51
78
|
return mergeFilterRule(baseRule, override);
|
|
52
79
|
};
|
|
53
80
|
|
|
81
|
+
type ClientRequestCaptureFlags = Pick<
|
|
82
|
+
TraceClientRequestCaptureRule,
|
|
83
|
+
'requestHeaders' | 'requestBody' | 'responseHeaders' | 'responseBody'
|
|
84
|
+
>;
|
|
85
|
+
|
|
86
|
+
const resolveClientRequestCaptureFlags = (
|
|
87
|
+
base?: TraceClientRequestCaptureRule,
|
|
88
|
+
override?: TraceClientRequestCaptureRule
|
|
89
|
+
): ClientRequestCaptureFlags => {
|
|
90
|
+
return {
|
|
91
|
+
requestHeaders: override?.requestHeaders ?? base?.requestHeaders,
|
|
92
|
+
requestBody: override?.requestBody ?? base?.requestBody,
|
|
93
|
+
responseHeaders: override?.responseHeaders ?? base?.responseHeaders,
|
|
94
|
+
responseBody: override?.responseBody ?? base?.responseBody,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const hasClientRequestCaptureFlags = (flags: ClientRequestCaptureFlags): boolean => {
|
|
99
|
+
return Object.values(flags).some((value) => value !== undefined);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const buildClientRequestCaptureRule = (
|
|
103
|
+
mergedRule: TraceFilterRule | undefined,
|
|
104
|
+
flags: ClientRequestCaptureFlags
|
|
105
|
+
): TraceClientRequestCaptureRule => {
|
|
106
|
+
const baseRule = mergedRule ? { ...mergedRule } : {};
|
|
107
|
+
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
...baseRule,
|
|
110
|
+
...(flags.requestHeaders === undefined ? {} : { requestHeaders: flags.requestHeaders }),
|
|
111
|
+
...(flags.requestBody === undefined ? {} : { requestBody: flags.requestBody }),
|
|
112
|
+
...(flags.responseHeaders === undefined ? {} : { responseHeaders: flags.responseHeaders }),
|
|
113
|
+
...(flags.responseBody === undefined ? {} : { responseBody: flags.responseBody }),
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const mergeClientRequestCaptureRule = (
|
|
118
|
+
base?: TraceClientRequestCaptureRule,
|
|
119
|
+
override?: TraceClientRequestCaptureRule
|
|
120
|
+
): TraceClientRequestCaptureRule | undefined => {
|
|
121
|
+
const mergedRule = mergeFilterRule(base, override);
|
|
122
|
+
const flags = resolveClientRequestCaptureFlags(base, override);
|
|
123
|
+
|
|
124
|
+
if (mergedRule === undefined && !hasClientRequestCaptureFlags(flags)) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return buildClientRequestCaptureRule(mergedRule, flags);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const collectClientRequestSourceKeys = (
|
|
132
|
+
base?: TraceClientRequestWatcherToggle,
|
|
133
|
+
override?: Exclude<TraceClientRequestWatcherToggle, boolean>
|
|
134
|
+
): string[] => {
|
|
135
|
+
const overrideSources = override?.sources ?? {};
|
|
136
|
+
const sourceKeys = new Set<string>([
|
|
137
|
+
...Object.keys(isObjectValue(base) ? base.sources ?? {} : {}),
|
|
138
|
+
...Object.keys(overrideSources),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
return [...sourceKeys];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const mergeClientRequestSources = (
|
|
145
|
+
base?: TraceClientRequestWatcherToggle,
|
|
146
|
+
override?: Exclude<TraceClientRequestWatcherToggle, boolean>
|
|
147
|
+
): Record<string, TraceClientRequestCaptureRule> | undefined => {
|
|
148
|
+
if (override === undefined) return undefined;
|
|
149
|
+
|
|
150
|
+
const sources: Record<string, TraceClientRequestCaptureRule> = {};
|
|
151
|
+
|
|
152
|
+
for (const key of collectClientRequestSourceKeys(base, override)) {
|
|
153
|
+
const baseSources = isObjectValue(base) ? base.sources : undefined;
|
|
154
|
+
const sourceRule = mergeClientRequestCaptureRule(baseSources?.[key], override.sources?.[key]);
|
|
155
|
+
if (sourceRule !== undefined) {
|
|
156
|
+
sources[key] = sourceRule;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return Object.keys(sources).length === 0 ? undefined : sources;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const mergeClientRequestWatcherToggle = (
|
|
164
|
+
base?: TraceClientRequestWatcherToggle,
|
|
165
|
+
override?: TraceClientRequestWatcherToggle
|
|
166
|
+
): TraceClientRequestWatcherToggle | undefined => {
|
|
167
|
+
if (override === undefined) return base;
|
|
168
|
+
if (override === false || override === true) return override;
|
|
169
|
+
|
|
170
|
+
const baseConfig = isObjectValue(base) ? base : undefined;
|
|
171
|
+
const merged = mergeClientRequestCaptureRule(baseConfig, override) ?? {};
|
|
172
|
+
const sources = mergeClientRequestSources(base, override);
|
|
173
|
+
|
|
174
|
+
if (sources === undefined) {
|
|
175
|
+
return merged;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return Object.freeze({
|
|
179
|
+
...merged,
|
|
180
|
+
sources,
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
54
184
|
const REQUEST_METHOD_KEYS = ['all', 'get', 'post', 'put', 'patch', 'delete'] as const;
|
|
55
185
|
|
|
56
186
|
const mergeRequestWatcherToggle = (
|
|
@@ -99,7 +229,7 @@ const mergeWatchers = (
|
|
|
99
229
|
batch: mergeWatcherToggle(base.batch, override.batch),
|
|
100
230
|
dump: mergeWatcherToggle(base.dump, override.dump),
|
|
101
231
|
view: mergeWatcherToggle(base.view, override.view),
|
|
102
|
-
clientRequest:
|
|
232
|
+
clientRequest: mergeClientRequestWatcherToggle(base.clientRequest, override.clientRequest),
|
|
103
233
|
};
|
|
104
234
|
};
|
|
105
235
|
|
package/src/dashboard/ui.ts
CHANGED
|
@@ -38,6 +38,7 @@ const SUN_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke
|
|
|
38
38
|
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>`;
|
|
39
39
|
|
|
40
40
|
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>`;
|
|
41
|
+
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>`;
|
|
41
42
|
|
|
42
43
|
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+)?`;
|
|
43
44
|
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+)?)`;
|
|
@@ -137,6 +138,10 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
137
138
|
const SUN_ICON = __TRACE_SUN_ICON__;
|
|
138
139
|
const MOON_ICON = __TRACE_MOON_ICON__;
|
|
139
140
|
const COPY_ICON = __TRACE_COPY_ICON__;
|
|
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)}
|
|
140
145
|
const JSON_HIGHLIGHT_PATTERN = new RegExp(__TRACE_JSON_REGEX__, 'g');
|
|
141
146
|
const SQL_HIGHLIGHT_PATTERN = new RegExp(__TRACE_SQL_REGEX__, 'gim');
|
|
142
147
|
const ENTRY_TYPES = ['request','query','exception','log','job','cache','schedule','mail','auth','event','model','notification','redis','gate','middleware','command','batch','dump','view','client_request'];
|
|
@@ -651,6 +656,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
651
656
|
return [
|
|
652
657
|
'<details class="trace-item trace-disclosure"' + (isInitiallyOpen ? ' open' : '') + '>',
|
|
653
658
|
'<summary class="trace-item-head trace-summary">',
|
|
659
|
+
'<span class="trace-summary-icon">' + DISCLOSURE_ICON + '</span>',
|
|
654
660
|
'<span class="trace-summary-main">',
|
|
655
661
|
'<span><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></span>',
|
|
656
662
|
'<span class="trace-summary-copy">' + entrySummaryInlineHtml(entry) + '</span>',
|
|
@@ -1083,6 +1089,7 @@ const buildDashboardHtml = (basePath: string, projectName?: string): string => {
|
|
|
1083
1089
|
.replace('__TRACE_SUN_ICON__', JSON.stringify(SUN_ICON))
|
|
1084
1090
|
.replace('__TRACE_MOON_ICON__', JSON.stringify(MOON_ICON))
|
|
1085
1091
|
.replace('__TRACE_COPY_ICON__', JSON.stringify(COPY_ICON))
|
|
1092
|
+
.replace('__TRACE_DISCLOSURE_ICON__', JSON.stringify(DISCLOSURE_ICON))
|
|
1086
1093
|
.replace('__TRACE_JSON_REGEX__', JSON.stringify(JSON_HIGHLIGHT_PATTERN))
|
|
1087
1094
|
.replace('__TRACE_SQL_REGEX__', JSON.stringify(SQL_HIGHLIGHT_PATTERN))
|
|
1088
1095
|
.replace('__TRACE_BASE_PATH_LABEL__', basePath)
|
|
@@ -27,6 +27,87 @@ const describeValueType = (value: unknown): string => {
|
|
|
27
27
|
return typeof value;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
type TracePathSegment = string | number;
|
|
31
|
+
|
|
32
|
+
type TracePathCandidate = {
|
|
33
|
+
path: TracePathSegment[];
|
|
34
|
+
size: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const chooseLargerCandidate = (
|
|
38
|
+
left: TracePathCandidate | null,
|
|
39
|
+
right: TracePathCandidate | null
|
|
40
|
+
): TracePathCandidate | null => {
|
|
41
|
+
if (left === null) return right;
|
|
42
|
+
if (right === null) return left;
|
|
43
|
+
return right.size > left.size ? right : left;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const fallbackCandidate = (value: unknown, path: TracePathSegment[]): TracePathCandidate | null => {
|
|
47
|
+
return path.length === 0 ? null : { path, size: serializedSize(value) };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const findLargestDroppablePathInArray = (
|
|
51
|
+
value: unknown[],
|
|
52
|
+
path: TracePathSegment[]
|
|
53
|
+
): TracePathCandidate | null => {
|
|
54
|
+
let best: TracePathCandidate | null = null;
|
|
55
|
+
|
|
56
|
+
for (const [index, item] of value.entries()) {
|
|
57
|
+
best = chooseLargerCandidate(best, findLargestDroppablePath(item, [...path, index]));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return best ?? fallbackCandidate(value, path);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const findLargestDroppablePathInObject = (
|
|
64
|
+
value: Record<string, unknown>,
|
|
65
|
+
path: TracePathSegment[]
|
|
66
|
+
): TracePathCandidate | null => {
|
|
67
|
+
let best: TracePathCandidate | null = null;
|
|
68
|
+
|
|
69
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
70
|
+
if (key === '__traceNotice') continue;
|
|
71
|
+
best = chooseLargerCandidate(best, findLargestDroppablePath(entryValue, [...path, key]));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return best ?? fallbackCandidate(value, path);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const findLargestDroppablePath = (
|
|
78
|
+
value: unknown,
|
|
79
|
+
path: TracePathSegment[] = []
|
|
80
|
+
): TracePathCandidate | null => {
|
|
81
|
+
if (Array.isArray(value)) return findLargestDroppablePathInArray(value, path);
|
|
82
|
+
if (typeof value === 'object' && value !== null) {
|
|
83
|
+
return findLargestDroppablePathInObject(value as Record<string, unknown>, path);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return fallbackCandidate(value, path);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const replaceAtPath = (value: unknown, path: TracePathSegment[], replacement: unknown): unknown => {
|
|
90
|
+
if (path.length === 0) return replacement;
|
|
91
|
+
|
|
92
|
+
const [segment, ...rest] = path;
|
|
93
|
+
|
|
94
|
+
if (Array.isArray(value) && typeof segment === 'number') {
|
|
95
|
+
const next = value.slice();
|
|
96
|
+
next[segment] = replaceAtPath(next[segment], rest, replacement);
|
|
97
|
+
return next;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof value === 'object' && value !== null && typeof segment === 'string') {
|
|
101
|
+
const current = value as Record<string, unknown>;
|
|
102
|
+
return {
|
|
103
|
+
...current,
|
|
104
|
+
[segment]: replaceAtPath(current[segment], rest, replacement),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return value;
|
|
109
|
+
};
|
|
110
|
+
|
|
30
111
|
const compactValue = (value: unknown, depth: number): unknown => {
|
|
31
112
|
if (depth >= DEFAULT_MAX_DEPTH) {
|
|
32
113
|
return DROPPED_FIELD_MESSAGE;
|
|
@@ -69,19 +150,19 @@ const compactValue = (value: unknown, depth: number): unknown => {
|
|
|
69
150
|
return Object.fromEntries(compactedEntries);
|
|
70
151
|
};
|
|
71
152
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
compacted
|
|
153
|
+
const compactStructuredValueToBudget = (value: unknown): unknown => {
|
|
154
|
+
let compacted: unknown =
|
|
155
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
156
|
+
? {
|
|
157
|
+
...(value as Record<string, unknown>),
|
|
158
|
+
__traceNotice: COMPACTED_CONTENT_MESSAGE,
|
|
159
|
+
}
|
|
160
|
+
: value;
|
|
161
|
+
|
|
162
|
+
while (serializedSize(compacted) > DEFAULT_MAX_ENTRY_BYTES) {
|
|
163
|
+
const candidate = findLargestDroppablePath(compacted);
|
|
164
|
+
if (candidate === null) break;
|
|
165
|
+
compacted = replaceAtPath(compacted, candidate.path, DROPPED_FIELD_MESSAGE);
|
|
85
166
|
}
|
|
86
167
|
|
|
87
168
|
return compacted;
|
|
@@ -97,10 +178,10 @@ const fitContentToBudget = (content: unknown): unknown => {
|
|
|
97
178
|
return compacted;
|
|
98
179
|
}
|
|
99
180
|
|
|
100
|
-
if (typeof compacted === 'object' && compacted !== null
|
|
101
|
-
const
|
|
102
|
-
if (serializedSize(
|
|
103
|
-
return
|
|
181
|
+
if (typeof compacted === 'object' && compacted !== null) {
|
|
182
|
+
const budgetCompacted = compactStructuredValueToBudget(compacted);
|
|
183
|
+
if (serializedSize(budgetCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
184
|
+
return budgetCompacted;
|
|
104
185
|
}
|
|
105
186
|
}
|
|
106
187
|
|
package/src/types.ts
CHANGED
|
@@ -209,6 +209,7 @@ export interface ViewContent {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
export interface ClientRequestContent {
|
|
212
|
+
source?: string;
|
|
212
213
|
method: string;
|
|
213
214
|
url: string;
|
|
214
215
|
requestHeaders: Record<string, string>;
|
|
@@ -222,6 +223,7 @@ export interface ClientRequestContent {
|
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
export interface ClientRequestTraceInput {
|
|
226
|
+
source?: string;
|
|
225
227
|
method: string;
|
|
226
228
|
url: string;
|
|
227
229
|
requestHeaders: Record<string, string>;
|
|
@@ -315,6 +317,13 @@ export type TraceFilterRule = {
|
|
|
315
317
|
exclude?: string[];
|
|
316
318
|
};
|
|
317
319
|
|
|
320
|
+
export type TraceClientRequestCaptureRule = TraceFilterRule & {
|
|
321
|
+
requestHeaders?: boolean;
|
|
322
|
+
requestBody?: boolean;
|
|
323
|
+
responseHeaders?: boolean;
|
|
324
|
+
responseBody?: boolean;
|
|
325
|
+
};
|
|
326
|
+
|
|
318
327
|
export type TraceRequestWatcherConfig = TraceFilterRule & {
|
|
319
328
|
all?: TraceFilterRule;
|
|
320
329
|
get?: TraceFilterRule;
|
|
@@ -324,8 +333,13 @@ export type TraceRequestWatcherConfig = TraceFilterRule & {
|
|
|
324
333
|
delete?: TraceFilterRule;
|
|
325
334
|
};
|
|
326
335
|
|
|
336
|
+
export type TraceClientRequestWatcherConfig = TraceClientRequestCaptureRule & {
|
|
337
|
+
sources?: Record<string, TraceClientRequestCaptureRule>;
|
|
338
|
+
};
|
|
339
|
+
|
|
327
340
|
export type TraceWatcherToggle = boolean | TraceFilterRule;
|
|
328
341
|
export type TraceRequestWatcherToggle = boolean | TraceRequestWatcherConfig;
|
|
342
|
+
export type TraceClientRequestWatcherToggle = boolean | TraceClientRequestWatcherConfig;
|
|
329
343
|
|
|
330
344
|
export type WatcherToggles = {
|
|
331
345
|
request?: TraceRequestWatcherToggle;
|
|
@@ -347,7 +361,7 @@ export type WatcherToggles = {
|
|
|
347
361
|
batch?: TraceWatcherToggle;
|
|
348
362
|
dump?: TraceWatcherToggle;
|
|
349
363
|
view?: TraceWatcherToggle;
|
|
350
|
-
clientRequest?:
|
|
364
|
+
clientRequest?: TraceClientRequestWatcherToggle;
|
|
351
365
|
};
|
|
352
366
|
|
|
353
367
|
export interface ITraceConfig {
|
package/src/utils/entryFilter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ITraceConfig,
|
|
3
3
|
ITraceEntry,
|
|
4
|
+
TraceClientRequestWatcherConfig,
|
|
4
5
|
TraceFilterRule,
|
|
5
6
|
TraceRequestWatcherConfig,
|
|
6
7
|
WatcherToggles,
|
|
@@ -22,6 +23,7 @@ const normalizeTerms = (terms?: string[]): string[] => {
|
|
|
22
23
|
|
|
23
24
|
const matchesRule = (haystack: string, rule?: TraceFilterRule): boolean => {
|
|
24
25
|
if (!rule) return true;
|
|
26
|
+
if (rule.enabled === false) return false;
|
|
25
27
|
|
|
26
28
|
const include = normalizeTerms(rule.include);
|
|
27
29
|
const exclude = normalizeTerms(rule.exclude);
|
|
@@ -86,6 +88,20 @@ const getRequestMethodRule = (
|
|
|
86
88
|
return watcher.all;
|
|
87
89
|
};
|
|
88
90
|
|
|
91
|
+
const getClientRequestSourceRule = (
|
|
92
|
+
watcher: TraceClientRequestWatcherConfig,
|
|
93
|
+
entry: ITraceEntry
|
|
94
|
+
): TraceFilterRule | undefined => {
|
|
95
|
+
if (entry.type !== EntryType.CLIENT_REQUEST) return undefined;
|
|
96
|
+
|
|
97
|
+
const content = isObjectValue(entry.content) ? entry.content : undefined;
|
|
98
|
+
const sourceValue = content?.['source'];
|
|
99
|
+
const source = typeof sourceValue === 'string' ? sourceValue.trim().toLowerCase() : '';
|
|
100
|
+
|
|
101
|
+
if (source === '') return undefined;
|
|
102
|
+
return watcher.sources?.[source];
|
|
103
|
+
};
|
|
104
|
+
|
|
89
105
|
export const TraceEntryFilter = Object.freeze({
|
|
90
106
|
shouldCapture(entry: ITraceEntry, config: ITraceConfig): boolean {
|
|
91
107
|
const watcherKey = watcherKeyByEntryType[entry.type];
|
|
@@ -103,6 +119,13 @@ export const TraceEntryFilter = Object.freeze({
|
|
|
103
119
|
if (!matchesRule(haystack, methodRule)) return false;
|
|
104
120
|
}
|
|
105
121
|
|
|
122
|
+
if (watcherKey === 'clientRequest') {
|
|
123
|
+
const clientRequestWatcher = watcher as TraceClientRequestWatcherConfig;
|
|
124
|
+
const sourceRule = getClientRequestSourceRule(clientRequestWatcher, entry);
|
|
125
|
+
if (sourceRule?.enabled === false) return false;
|
|
126
|
+
if (!matchesRule(haystack, sourceRule)) return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
return true;
|
|
107
130
|
},
|
|
108
131
|
});
|
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
ClientRequestTraceInput,
|
|
5
5
|
ITraceWatcher,
|
|
6
6
|
ITraceWatcherConfig,
|
|
7
|
+
TraceClientRequestCaptureRule,
|
|
8
|
+
TraceClientRequestWatcherConfig,
|
|
7
9
|
} from '../types';
|
|
8
10
|
import { EntryType } from '../types';
|
|
9
11
|
import { AuthTag } from '../utils/authTag';
|
|
@@ -14,8 +16,105 @@ let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
|
14
16
|
let _redactHeaderNames: string[] = [];
|
|
15
17
|
let _redactBodyFields: string[] = [];
|
|
16
18
|
let _ignoreRoutes: string[] = [];
|
|
19
|
+
let _clientRequestWatcher: TraceClientRequestWatcherConfig | undefined;
|
|
20
|
+
|
|
21
|
+
const isObjectValue = (value: unknown): value is Record<string, unknown> => {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const resolveSource = (value: unknown): string | undefined => {
|
|
26
|
+
if (typeof value !== 'string') return undefined;
|
|
27
|
+
const normalized = value.trim().toLowerCase();
|
|
28
|
+
return normalized === '' ? undefined : normalized;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const resolveSourceRule = (
|
|
32
|
+
source: string | undefined
|
|
33
|
+
): TraceClientRequestCaptureRule | undefined => {
|
|
34
|
+
if (source === undefined) return undefined;
|
|
35
|
+
return _clientRequestWatcher?.sources?.[source];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const shouldCaptureField = (
|
|
39
|
+
field: keyof Pick<
|
|
40
|
+
TraceClientRequestCaptureRule,
|
|
41
|
+
'requestHeaders' | 'requestBody' | 'responseHeaders' | 'responseBody'
|
|
42
|
+
>,
|
|
43
|
+
sourceRule: TraceClientRequestCaptureRule | undefined
|
|
44
|
+
): boolean => {
|
|
45
|
+
const scoped = sourceRule?.[field];
|
|
46
|
+
if (typeof scoped === 'boolean') return scoped;
|
|
47
|
+
const global = _clientRequestWatcher?.[field];
|
|
48
|
+
if (typeof global === 'boolean') return global;
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const buildRequestHeaders = (
|
|
53
|
+
requestHeaders: Record<string, string>,
|
|
54
|
+
sourceRule: TraceClientRequestCaptureRule | undefined
|
|
55
|
+
): Pick<ClientRequestContent, 'requestHeaders'> => {
|
|
56
|
+
return shouldCaptureField('requestHeaders', sourceRule)
|
|
57
|
+
? { requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames) }
|
|
58
|
+
: { requestHeaders: {} };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const buildRequestBody = (
|
|
62
|
+
requestBody: unknown,
|
|
63
|
+
sourceRule: TraceClientRequestCaptureRule | undefined
|
|
64
|
+
): Partial<Pick<ClientRequestContent, 'requestBody'>> => {
|
|
65
|
+
if (requestBody === undefined) return {};
|
|
66
|
+
if (!shouldCaptureField('requestBody', sourceRule)) return {};
|
|
67
|
+
return { requestBody: redactUnknown(requestBody, _redactBodyFields) };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const buildResponseHeaders = (
|
|
71
|
+
responseHeaders: Record<string, string> | undefined,
|
|
72
|
+
sourceRule: TraceClientRequestCaptureRule | undefined
|
|
73
|
+
): Partial<Pick<ClientRequestContent, 'responseHeaders'>> => {
|
|
74
|
+
if (responseHeaders === undefined) return {};
|
|
75
|
+
if (!shouldCaptureField('responseHeaders', sourceRule)) return {};
|
|
76
|
+
return { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const buildResponseBody = (
|
|
80
|
+
responseBody: unknown,
|
|
81
|
+
sourceRule: TraceClientRequestCaptureRule | undefined
|
|
82
|
+
): Partial<Pick<ClientRequestContent, 'responseBody'>> => {
|
|
83
|
+
if (responseBody === undefined) return {};
|
|
84
|
+
if (!shouldCaptureField('responseBody', sourceRule)) return {};
|
|
85
|
+
return { responseBody: redactUnknown(responseBody, _redactBodyFields) };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const buildClientRequestContent = (
|
|
89
|
+
input: ClientRequestTraceInput,
|
|
90
|
+
sourceRule: TraceClientRequestCaptureRule | undefined,
|
|
91
|
+
normalizedSource: string | undefined
|
|
92
|
+
): ClientRequestContent => {
|
|
93
|
+
return {
|
|
94
|
+
...(normalizedSource === undefined ? {} : { source: normalizedSource }),
|
|
95
|
+
method: input.method.toUpperCase(),
|
|
96
|
+
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 } : {}),
|
|
103
|
+
duration: input.duration,
|
|
104
|
+
hostname: TraceContext.getHostname(),
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const isWatcherEnabled = (
|
|
109
|
+
value: ITraceWatcherConfig['config']['watchers']['clientRequest']
|
|
110
|
+
): boolean => {
|
|
111
|
+
if (value === false) return false;
|
|
112
|
+
if (isObjectValue(value) && value.enabled === false) return false;
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
17
115
|
|
|
18
116
|
const emit = ({
|
|
117
|
+
source,
|
|
19
118
|
method,
|
|
20
119
|
url,
|
|
21
120
|
requestHeaders,
|
|
@@ -28,26 +127,28 @@ const emit = ({
|
|
|
28
127
|
}: ClientRequestTraceInput): void => {
|
|
29
128
|
if (!_storage) return;
|
|
30
129
|
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
130
|
+
const normalizedSource = resolveSource(source);
|
|
131
|
+
const sourceRule = resolveSourceRule(normalizedSource);
|
|
132
|
+
if (sourceRule?.enabled === false) return;
|
|
31
133
|
const tags = AuthTag.append([method.toUpperCase()]);
|
|
32
134
|
if ((responseStatus ?? 0) >= 400 || error) tags.push('failed');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
135
|
+
if (normalizedSource !== undefined) tags.push(normalizedSource);
|
|
136
|
+
const content = buildClientRequestContent(
|
|
137
|
+
{
|
|
138
|
+
source,
|
|
139
|
+
method,
|
|
140
|
+
url,
|
|
141
|
+
requestHeaders,
|
|
142
|
+
responseStatus,
|
|
143
|
+
duration,
|
|
144
|
+
requestBody,
|
|
145
|
+
responseHeaders,
|
|
146
|
+
responseBody,
|
|
147
|
+
error,
|
|
148
|
+
},
|
|
149
|
+
sourceRule,
|
|
150
|
+
normalizedSource
|
|
151
|
+
);
|
|
51
152
|
_storage
|
|
52
153
|
.writeEntry({
|
|
53
154
|
uuid: crypto.randomUUID(),
|
|
@@ -64,13 +165,18 @@ const emit = ({
|
|
|
64
165
|
export const HttpClientWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
|
|
65
166
|
emit,
|
|
66
167
|
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
67
|
-
if (config.watchers.clientRequest
|
|
168
|
+
if (!isWatcherEnabled(config.watchers.clientRequest)) return () => undefined;
|
|
68
169
|
_storage = storage;
|
|
170
|
+
_clientRequestWatcher =
|
|
171
|
+
typeof config.watchers.clientRequest === 'object' && config.watchers.clientRequest !== null
|
|
172
|
+
? (config.watchers.clientRequest as TraceClientRequestWatcherConfig)
|
|
173
|
+
: undefined;
|
|
69
174
|
_redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
|
|
70
175
|
_redactBodyFields = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
|
|
71
176
|
_ignoreRoutes = config.ignoreRoutes;
|
|
72
177
|
return () => {
|
|
73
178
|
_storage = null;
|
|
179
|
+
_clientRequestWatcher = undefined;
|
|
74
180
|
_redactBodyFields = [];
|
|
75
181
|
_ignoreRoutes = [];
|
|
76
182
|
};
|