@zintrust/trace 0.4.95 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.5.0",
4
+ "buildDate": "2026-04-12T15:38:51.982Z",
5
5
  "buildEnvironment": {
6
6
  "node": "v22.22.1",
7
7
  "platform": "darwin",
8
8
  "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "8f4c9f2f",
11
+ "commit": "d9f9cf02",
12
12
  "branch": "release"
13
13
  },
14
14
  "package": {
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "files": {
24
24
  "build-manifest.json": {
25
- "size": 14440,
26
- "sha256": "cbd454ac88211f4242ab1d62b641b064452d5532792e5ecae7cbe2a49f4d652a"
25
+ "size": 14739,
26
+ "sha256": "ccb926d22667a613e35b97c316d5924b223b5522c5f7deab259d99343fe4c8d8"
27
27
  },
28
28
  "cli-register.d.ts": {
29
29
  "size": 255,
@@ -38,8 +38,8 @@
38
38
  "sha256": "b034cbef0c71fb868071363624ef7a9f8d7acc20f8be8c895dd5db5a75e81f37"
39
39
  },
40
40
  "config.js": {
41
- "size": 5961,
42
- "sha256": "1402cf8ad7c850da99e429c8c937385b7b5cf401fa3714d972d4e6bd94f1663e"
41
+ "size": 9272,
42
+ "sha256": "d23145038d47ce94a51394088b2b6a3138ec232802bde7bd521d347c9bdfb310"
43
43
  },
44
44
  "context.d.ts": {
45
45
  "size": 596,
@@ -54,8 +54,8 @@
54
54
  "sha256": "430f5b294d960e13e2ec39ed5c22f6655f85c46c2de9455c222505c67298ec6a"
55
55
  },
56
56
  "dashboard/handlers.js": {
57
- "size": 5390,
58
- "sha256": "a8644a932c2da07231098f9adc31cfca00727b448524107ce23ad87a239f53f3"
57
+ "size": 9192,
58
+ "sha256": "ebc592e84772b93c820c76f1834e1131a05c19df3bada7b75cf46bcd5f6732fc"
59
59
  },
60
60
  "dashboard/routes.d.ts": {
61
61
  "size": 997,
@@ -70,16 +70,16 @@
70
70
  "sha256": "4862b41e0477f01afa0dbb446d4553b65c22ed774cd1e2db3489059ced392f94"
71
71
  },
72
72
  "dashboard/ui.js": {
73
- "size": 74630,
74
- "sha256": "925ab75a6176dc4639d0772eda904f1401d73e866f0f0882450d9fd4e253ad89"
73
+ "size": 75051,
74
+ "sha256": "941c8647e778f67b8ab3f37b9272b914e5da4f35e07511ff1112287a0620b6da"
75
75
  },
76
76
  "index.d.ts": {
77
77
  "size": 2537,
78
78
  "sha256": "1707d26322dbad17f6bf85938ae6fe2477e84c7fed3333760ce8d6eadfaaffd2"
79
79
  },
80
80
  "index.js": {
81
- "size": 3325,
82
- "sha256": "3715aa0dca22638b15c2ec534e2463d7c82268e7648f75571c591d07edc9a0de"
81
+ "size": 3324,
82
+ "sha256": "8a10c92829328ecb61fadb66c54768b52d72956fb196ad9a0087ba1ad9b17cbd"
83
83
  },
84
84
  "migrations/20260331000001_create_zin_trace_entries_table.d.ts": {
85
85
  "size": 304,
@@ -150,8 +150,8 @@
150
150
  "sha256": "d899a615e6cf2a5eea51f6200347cb81fbaae11979b801859f57135083aaf85b"
151
151
  },
152
152
  "storage/TraceContentBudget.js": {
153
- "size": 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": 346,
326
+ "sha256": "dfb13bba526d5338e4dcd7a5aa0f72a61a03d9e2f6a250250c0bb8f0054c9712"
327
327
  },
328
328
  "watchers/HttpClientWatcher.js": {
329
- "size": 2414,
330
- "sha256": "817c74e7a89bcd0c53c0344d713b422e0c9e51ec65e7b6c97f0c486116dda7a5"
329
+ "size": 5829,
330
+ "sha256": "3921cfb79cd5ff0da8af90ee66a5ad3b76a397e60df808885d6ecc84b5269810"
331
331
  },
332
332
  "watchers/HttpWatcher.d.ts": {
333
333
  "size": 96,
package/dist/config.js CHANGED
@@ -12,15 +12,26 @@ const mergeStringLists = (base, override) => {
12
12
  const isObjectValue = (value) => {
13
13
  return typeof value === 'object' && value !== null && !Array.isArray(value);
14
14
  };
15
+ const resolveEnabled = (base, override) => {
16
+ return override?.enabled ?? base?.enabled;
17
+ };
18
+ const hasMergedRuleValues = (include, exclude, enabled) => {
19
+ return include.length > 0 || exclude.length > 0 || enabled !== undefined;
20
+ };
21
+ const buildFilterRule = (input) => {
22
+ return Object.freeze({
23
+ ...(input.enabled === undefined ? {} : { enabled: input.enabled }),
24
+ ...(input.include.length > 0 ? { include: input.include } : {}),
25
+ ...(input.exclude.length > 0 ? { exclude: input.exclude } : {}),
26
+ });
27
+ };
15
28
  const mergeFilterRule = (base, override) => {
16
29
  const include = mergeStringLists(base?.include ?? [], override?.include);
17
30
  const exclude = mergeStringLists(base?.exclude ?? [], override?.exclude);
18
- 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({
@@ -38,24 +38,127 @@ const getNumericQueryParam = (req, key) => {
38
38
  }
39
39
  return undefined;
40
40
  };
41
+ const DEFAULT_PER_PAGE = 50;
42
+ const MAX_PER_PAGE = 100;
43
+ const DEFAULT_REQUEST_PER_PAGE = 25;
44
+ const MAX_REQUEST_PER_PAGE = 50;
45
+ const SUMMARY_TEXT_LIMIT = 280;
46
+ const SUMMARY_ARRAY_LIMIT = 10;
47
+ const truncateText = (value, limit = SUMMARY_TEXT_LIMIT) => value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 3))}...`;
48
+ const compactValue = (value) => {
49
+ if (typeof value === 'string') {
50
+ return truncateText(value);
51
+ }
52
+ if (typeof value === 'number' ||
53
+ typeof value === 'boolean' ||
54
+ value === null ||
55
+ value === undefined) {
56
+ return value;
57
+ }
58
+ if (Array.isArray(value)) {
59
+ return value.slice(0, SUMMARY_ARRAY_LIMIT).map((item) => {
60
+ if (typeof item === 'string') {
61
+ return truncateText(item);
62
+ }
63
+ if (typeof item === 'number' || typeof item === 'boolean' || item === null) {
64
+ return item;
65
+ }
66
+ return '[complex]';
67
+ });
68
+ }
69
+ return undefined;
70
+ };
71
+ const pickCompactContent = (content, keys) => {
72
+ if (typeof content !== 'object' || content === null || Array.isArray(content)) {
73
+ return {};
74
+ }
75
+ const source = content;
76
+ const compact = {};
77
+ for (const key of keys) {
78
+ const value = compactValue(source[key]);
79
+ if (value !== undefined) {
80
+ compact[key] = value;
81
+ }
82
+ }
83
+ return compact;
84
+ };
85
+ const COMPACT_ENTRY_KEYS = {
86
+ request: [
87
+ 'method',
88
+ 'uri',
89
+ 'responseStatus',
90
+ 'duration',
91
+ 'memory',
92
+ 'middleware',
93
+ 'hostname',
94
+ 'userId',
95
+ ],
96
+ query: ['connection', 'sql', 'time', 'duration', 'slow', 'hash', 'hostname'],
97
+ exception: ['class', 'file', 'line', 'message', 'occurrences', 'hostname', 'userId'],
98
+ log: ['level', 'message', 'hostname'],
99
+ job: ['status', 'connection', 'queue', 'name', 'tries', 'timeout', 'hostname'],
100
+ cache: ['operation', 'key', 'hit', 'store', 'payloadLogged', 'ttl', 'duration', 'hostname'],
101
+ schedule: ['name', 'expression', 'status', 'duration', 'hostname'],
102
+ mail: ['to', 'subject', 'template', 'hostname'],
103
+ auth: ['event', 'userId', 'hostname'],
104
+ event: ['name', 'listenerCount', 'hostname'],
105
+ model: ['action', 'model', 'id', 'hostname'],
106
+ notification: ['channels', 'notifiable', 'notification', 'message', 'hostname'],
107
+ redis: ['command', 'duration', 'hostname'],
108
+ gate: ['ability', 'result', 'userId', 'subject', 'hostname'],
109
+ middleware: ['name', 'event', 'duration', 'hostname'],
110
+ command: ['name', 'exitCode', 'duration', 'hostname'],
111
+ batch: ['name', 'total', 'processed', 'failed', 'status', 'hostname'],
112
+ dump: ['file', 'line', 'hostname'],
113
+ view: ['template', 'duration', 'hostname'],
114
+ client_request: ['source', 'method', 'url', 'responseStatus', 'error', 'duration', 'hostname'],
115
+ };
116
+ const compactEntryContent = (entry) => pickCompactContent(entry.content, COMPACT_ENTRY_KEYS[entry.type]);
117
+ const estimateContentBytes = (content) => {
118
+ try {
119
+ return new TextEncoder().encode(JSON.stringify(content)).length;
120
+ }
121
+ catch {
122
+ return undefined;
123
+ }
124
+ };
125
+ const compactListEntry = (entry) => ({
126
+ ...entry,
127
+ content: compactEntryContent(entry),
128
+ hasDetails: true,
129
+ contentBytes: estimateContentBytes(entry.content),
130
+ });
131
+ const resolvePerPage = (req, type) => {
132
+ const isRequestList = type === 'request';
133
+ const fallback = isRequestList ? DEFAULT_REQUEST_PER_PAGE : DEFAULT_PER_PAGE;
134
+ const limit = isRequestList ? MAX_REQUEST_PER_PAGE : MAX_PER_PAGE;
135
+ return Math.max(1, Math.min(qpInt(req, 'perPage', fallback), limit));
136
+ };
41
137
  // ---------------------------------------------------------------------------
42
138
  // Entry handlers
43
139
  // ---------------------------------------------------------------------------
44
140
  export async function listEntries(req, res) {
45
141
  const storage = getStorage(res);
46
142
  if (storage !== null) {
143
+ const type = qp(req, 'type');
47
144
  const opts = {
48
- type: qp(req, 'type'),
145
+ type,
49
146
  tag: qp(req, 'tag'),
50
147
  batchId: qp(req, 'batchId'),
51
148
  from: getNumericQueryParam(req, 'from'),
52
149
  to: getNumericQueryParam(req, 'to'),
53
- page: qpInt(req, 'page', 1),
54
- perPage: Math.min(qpInt(req, 'perPage', 50), 200),
150
+ page: Math.max(1, qpInt(req, 'page', 1)),
151
+ perPage: resolvePerPage(req, type),
55
152
  };
56
153
  try {
57
154
  const result = await storage.queryEntries(opts);
58
- res.json({ ok: true, ...result, page: opts.page, perPage: opts.perPage });
155
+ res.json({
156
+ ok: true,
157
+ data: result.data.map(compactListEntry),
158
+ total: result.total,
159
+ page: opts.page,
160
+ perPage: opts.perPage,
161
+ });
59
162
  }
60
163
  catch (err) {
61
164
  res.setStatus(500).json({ error: err.message });
@@ -35,6 +35,7 @@ const BRAND_SVG = `<svg width="120" height="120" viewBox="0 0 100 100" fill="non
35
35
  const SUN_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2.2M12 19.8V22M4.93 4.93l1.56 1.56M17.51 17.51l1.56 1.56M2 12h2.2M19.8 12H22M4.93 19.07l1.56-1.56M17.51 6.49l1.56-1.56"></path></svg>`;
36
36
  const MOON_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"></path></svg>`;
37
37
  const COPY_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
38
+ const DISCLOSURE_ICON = `<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4l6 6-6 6"></path></svg>`;
38
39
  const JSON_HIGHLIGHT_PATTERN = String.raw `("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")(?=\s*:)|(\s*:)|("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?`;
39
40
  const SQL_HIGHLIGHT_PATTERN = String.raw `(\/\*[\s\S]*?\*\/|--.*$|'(?:''|[^'])*'|\x60[^\x60]+\x60|\b(?:select|from|where|insert|into|values|update|delete|join|left|right|inner|outer|on|and|or|limit|order|by|group|having|as|distinct|null|is|in|like|set|case|when|then|else|end|returning|union|all)\b|-?\d+(?:\.\d+)?)`;
40
41
  const encodeSvgDataUri = (svg) => {
@@ -131,6 +132,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
131
132
  const SUN_ICON = __TRACE_SUN_ICON__;
132
133
  const MOON_ICON = __TRACE_MOON_ICON__;
133
134
  const COPY_ICON = __TRACE_COPY_ICON__;
135
+ const DISCLOSURE_ICON = __TRACE_DISCLOSURE_ICON__;
134
136
  const JSON_HIGHLIGHT_PATTERN = new RegExp(__TRACE_JSON_REGEX__, 'g');
135
137
  const SQL_HIGHLIGHT_PATTERN = new RegExp(__TRACE_SQL_REGEX__, 'gim');
136
138
  const ENTRY_TYPES = ['request','query','exception','log','job','cache','schedule','mail','auth','event','model','notification','redis','gate','middleware','command','batch','dump','view','client_request'];
@@ -645,6 +647,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
645
647
  return [
646
648
  '<details class="trace-item trace-disclosure"' + (isInitiallyOpen ? ' open' : '') + '>',
647
649
  '<summary class="trace-item-head trace-summary">',
650
+ '<span class="trace-summary-icon">' + DISCLOSURE_ICON + '</span>',
648
651
  '<span class="trace-summary-main">',
649
652
  '<span><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></span>',
650
653
  '<span class="trace-summary-copy">' + entrySummaryInlineHtml(entry) + '</span>',
@@ -1075,6 +1078,7 @@ const buildDashboardHtml = (basePath, projectName) => {
1075
1078
  .replace('__TRACE_SUN_ICON__', JSON.stringify(SUN_ICON))
1076
1079
  .replace('__TRACE_MOON_ICON__', JSON.stringify(MOON_ICON))
1077
1080
  .replace('__TRACE_COPY_ICON__', JSON.stringify(COPY_ICON))
1081
+ .replace('__TRACE_DISCLOSURE_ICON__', JSON.stringify(DISCLOSURE_ICON))
1078
1082
  .replace('__TRACE_JSON_REGEX__', JSON.stringify(JSON_HIGHLIGHT_PATTERN))
1079
1083
  .replace('__TRACE_SQL_REGEX__', JSON.stringify(SQL_HIGHLIGHT_PATTERN))
1080
1084
  .replace('__TRACE_BASE_PATH_LABEL__', basePath)
@@ -21,6 +21,58 @@ const describeValueType = (value) => {
21
21
  return 'null';
22
22
  return typeof value;
23
23
  };
24
+ const chooseLargerCandidate = (left, right) => {
25
+ if (left === null)
26
+ return right;
27
+ if (right === null)
28
+ return left;
29
+ return right.size > left.size ? right : left;
30
+ };
31
+ const fallbackCandidate = (value, path) => {
32
+ return path.length === 0 ? null : { path, size: serializedSize(value) };
33
+ };
34
+ const findLargestDroppablePathInArray = (value, path) => {
35
+ let best = null;
36
+ for (const [index, item] of value.entries()) {
37
+ best = chooseLargerCandidate(best, findLargestDroppablePath(item, [...path, index]));
38
+ }
39
+ return best ?? fallbackCandidate(value, path);
40
+ };
41
+ const findLargestDroppablePathInObject = (value, path) => {
42
+ let best = null;
43
+ for (const [key, entryValue] of Object.entries(value)) {
44
+ if (key === '__traceNotice')
45
+ continue;
46
+ best = chooseLargerCandidate(best, findLargestDroppablePath(entryValue, [...path, key]));
47
+ }
48
+ return best ?? fallbackCandidate(value, path);
49
+ };
50
+ const findLargestDroppablePath = (value, path = []) => {
51
+ if (Array.isArray(value))
52
+ return findLargestDroppablePathInArray(value, path);
53
+ if (typeof value === 'object' && value !== null) {
54
+ return findLargestDroppablePathInObject(value, path);
55
+ }
56
+ return fallbackCandidate(value, path);
57
+ };
58
+ const replaceAtPath = (value, path, replacement) => {
59
+ if (path.length === 0)
60
+ return replacement;
61
+ const [segment, ...rest] = path;
62
+ if (Array.isArray(value) && typeof segment === 'number') {
63
+ const next = value.slice();
64
+ next[segment] = replaceAtPath(next[segment], rest, replacement);
65
+ return next;
66
+ }
67
+ if (typeof value === 'object' && value !== null && typeof segment === 'string') {
68
+ const current = value;
69
+ return {
70
+ ...current,
71
+ [segment]: replaceAtPath(current[segment], rest, replacement),
72
+ };
73
+ }
74
+ return value;
75
+ };
24
76
  const compactValue = (value, depth) => {
25
77
  if (depth >= DEFAULT_MAX_DEPTH) {
26
78
  return DROPPED_FIELD_MESSAGE;
@@ -52,18 +104,18 @@ const compactValue = (value, depth) => {
52
104
  }
53
105
  return Object.fromEntries(compactedEntries);
54
106
  };
55
- const 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
- import type { ClientRequestTraceInput, ITraceWatcher } from '../types';
2
- declare const emit: ({ method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }: ClientRequestTraceInput) => void;
1
+ import { type ClientRequestTraceInput, type ITraceWatcher } from '../types';
2
+ declare const emit: ({ source, method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }: ClientRequestTraceInput) => void;
3
3
  export declare const HttpClientWatcher: ITraceWatcher & {
4
4
  emit: typeof emit;
5
5
  };