@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.
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.4.95",
4
- "buildDate": "2026-04-11T17:50:25.235Z",
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": "8f4c9f2f",
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": 14440,
26
- "sha256": "cbd454ac88211f4242ab1d62b641b064452d5532792e5ecae7cbe2a49f4d652a"
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": 5961,
42
- "sha256": "1402cf8ad7c850da99e429c8c937385b7b5cf401fa3714d972d4e6bd94f1663e"
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": 74630,
74
- "sha256": "925ab75a6176dc4639d0772eda904f1401d73e866f0f0882450d9fd4e253ad89"
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": "3715aa0dca22638b15c2ec534e2463d7c82268e7648f75571c591d07edc9a0de"
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": 4022,
154
- "sha256": "4b1d4f0ad7da15caeaa1f9fe8b4edcd3000b0000c5bdc270601cd6db2eedce2e"
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": 8448,
198
- "sha256": "de994120c04696e08afb428cff27a99fb2918f5c4277b8190b8fb3d36d139f0d"
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": 3279,
226
- "sha256": "0ce8c5955411194447a16bd79c2f0a33ba60f63b2bd010ac33772718f02f0124"
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": 333,
326
- "sha256": "08ab7e213c489ecc4fdd3166d7a121b9a5220ff9cbae9841d4787d9a804e11ce"
325
+ "size": 341,
326
+ "sha256": "7e20bd9240de2165def5d90ec529e4da4e0a7302bc855bd2fb873f8b71d0182f"
327
327
  },
328
328
  "watchers/HttpClientWatcher.js": {
329
- "size": 2414,
330
- "sha256": "817c74e7a89bcd0c53c0344d713b422e0c9e51ec65e7b6c97f0c486116dda7a5"
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
- if (include.length === 0 && exclude.length === 0)
31
+ const enabled = resolveEnabled(base, override);
32
+ if (!hasMergedRuleValues(include, exclude, enabled))
19
33
  return undefined;
20
- return Object.freeze({
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: mergeWatcherToggle(base.clientRequest, override.clientRequest),
150
+ clientRequest: mergeClientRequestWatcherToggle(base.clientRequest, override.clientRequest),
74
151
  };
75
152
  };
76
153
  const DEFAULTS = Object.freeze({
@@ -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 compactTopLevelObjectToBudget = (value) => {
56
- const compacted = {
57
- ...value,
58
- __traceNotice: COMPACTED_CONTENT_MESSAGE,
59
- };
60
- const keysByDescendingSize = Object.keys(compacted)
61
- .filter((key) => key !== '__traceNotice')
62
- .sort((left, right) => serializedSize(compacted[right]) - serializedSize(compacted[left]));
63
- for (const key of keysByDescendingSize) {
64
- if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES)
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[key] = DROPPED_FIELD_MESSAGE;
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 && !Array.isArray(compacted)) {
79
- const topLevelCompacted = compactTopLevelObjectToBudget(compacted);
80
- if (serializedSize(topLevelCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
81
- return topLevelCompacted;
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?: TraceWatcherToggle;
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
- const emit = ({ method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }) => {
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
- const content = {
19
- method: method.toUpperCase(),
94
+ if (normalizedSource !== undefined)
95
+ tags.push(normalizedSource);
96
+ const content = buildClientRequestContent({
97
+ source,
98
+ method,
20
99
  url,
21
- requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames),
22
- ...(requestBody === undefined
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
- hostname: TraceContext.getHostname(),
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 === false)
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.95",
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.94"
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.length === 0 && exclude.length === 0) return undefined;
65
+ if (!hasMergedRuleValues(include, exclude, enabled)) return undefined;
36
66
 
37
- return Object.freeze({
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: mergeWatcherToggle(base.clientRequest, override.clientRequest),
232
+ clientRequest: mergeClientRequestWatcherToggle(base.clientRequest, override.clientRequest),
103
233
  };
104
234
  };
105
235
 
@@ -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 compactTopLevelObjectToBudget = (value: Record<string, unknown>): Record<string, unknown> => {
73
- const compacted: Record<string, unknown> = {
74
- ...value,
75
- __traceNotice: COMPACTED_CONTENT_MESSAGE,
76
- };
77
-
78
- const keysByDescendingSize = Object.keys(compacted)
79
- .filter((key) => key !== '__traceNotice')
80
- .sort((left, right) => serializedSize(compacted[right]) - serializedSize(compacted[left]));
81
-
82
- for (const key of keysByDescendingSize) {
83
- if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) break;
84
- compacted[key] = DROPPED_FIELD_MESSAGE;
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 && !Array.isArray(compacted)) {
101
- const topLevelCompacted = compactTopLevelObjectToBudget(compacted as Record<string, unknown>);
102
- if (serializedSize(topLevelCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
103
- return topLevelCompacted;
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?: TraceWatcherToggle;
364
+ clientRequest?: TraceClientRequestWatcherToggle;
351
365
  };
352
366
 
353
367
  export interface ITraceConfig {
@@ -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
- const content: ClientRequestContent = {
34
- method: method.toUpperCase(),
35
- url,
36
- requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames),
37
- ...(requestBody === undefined
38
- ? {}
39
- : { requestBody: redactUnknown(requestBody, _redactBodyFields) }),
40
- ...(responseStatus === undefined ? {} : { responseStatus }),
41
- ...(responseHeaders === undefined
42
- ? {}
43
- : { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) }),
44
- ...(responseBody === undefined
45
- ? {}
46
- : { responseBody: redactUnknown(responseBody, _redactBodyFields) }),
47
- ...(typeof error === 'string' && error !== '' ? { error } : {}),
48
- duration,
49
- hostname: TraceContext.getHostname(),
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 === false) return () => undefined;
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
  };