@zintrust/trace 0.4.94 → 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,15 +1,15 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.4.94",
4
- "buildDate": "2026-04-11T09:24:47.024Z",
3
+ "version": "0.4.96",
4
+ "buildDate": "2026-04-11T20:49:58.121Z",
5
5
  "buildEnvironment": {
6
- "node": "v20.20.2",
7
- "platform": "linux",
8
- "arch": "x64"
6
+ "node": "v22.22.1",
7
+ "platform": "darwin",
8
+ "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "785d0351",
12
- "branch": "master"
11
+ "commit": "ddf9b233",
12
+ "branch": "release"
13
13
  },
14
14
  "package": {
15
15
  "engines": {
@@ -21,6 +21,10 @@
21
21
  ]
22
22
  },
23
23
  "files": {
24
+ "build-manifest.json": {
25
+ "size": 14739,
26
+ "sha256": "b57ad8dd90ba688df31448cae52218fcfc3be48d533c55a06db0247741f30c8d"
27
+ },
24
28
  "cli-register.d.ts": {
25
29
  "size": 255,
26
30
  "sha256": "da8d689fe5ef32e97e755f28017e4d3cb1aa63489073a71907ea41ad5761ede9"
@@ -34,8 +38,8 @@
34
38
  "sha256": "b034cbef0c71fb868071363624ef7a9f8d7acc20f8be8c895dd5db5a75e81f37"
35
39
  },
36
40
  "config.js": {
37
- "size": 5961,
38
- "sha256": "1402cf8ad7c850da99e429c8c937385b7b5cf401fa3714d972d4e6bd94f1663e"
41
+ "size": 9270,
42
+ "sha256": "346361028d355391f3068c340db559dce558145e173bf248aa8d1c6328575de3"
39
43
  },
40
44
  "context.d.ts": {
41
45
  "size": 596,
@@ -66,8 +70,8 @@
66
70
  "sha256": "4862b41e0477f01afa0dbb446d4553b65c22ed774cd1e2db3489059ced392f94"
67
71
  },
68
72
  "dashboard/ui.js": {
69
- "size": 73577,
70
- "sha256": "2903901d8c0c5076118aa691727daa79be7abe87fdb393c5b389a2b1a8fce170"
73
+ "size": 79698,
74
+ "sha256": "60295315a1fbbfd02018a971399739971252ff16f252ddfea0ea43a562aa305f"
71
75
  },
72
76
  "index.d.ts": {
73
77
  "size": 2537,
@@ -75,7 +79,7 @@
75
79
  },
76
80
  "index.js": {
77
81
  "size": 3325,
78
- "sha256": "0926788b00fc68f513ae20a508f2ef3c2f4e7e907fa32d70f146ce6e8b0ec812"
82
+ "sha256": "fc1e557217d22f31fa29611bebd54d9a0d93393f9a425283f238d91a30b216c9"
79
83
  },
80
84
  "migrations/20260331000001_create_zin_trace_entries_table.d.ts": {
81
85
  "size": 304,
@@ -133,13 +137,21 @@
133
137
  "size": 14327,
134
138
  "sha256": "efc9bb131b9eef7e81a6f85ea3338fd8ac74257f794aa0fc7c76efed636f99d8"
135
139
  },
140
+ "storage/DebuggerStorage.d.ts": {
141
+ "size": 517,
142
+ "sha256": "c9c215aaa414f7b0c1fec6c82b054fc52bdf97af58f96f35c7f96672fb859c31"
143
+ },
144
+ "storage/DebuggerStorage.js": {
145
+ "size": 7442,
146
+ "sha256": "5ecce0fcfcf695df587a7b90a7a5c7efd2e64ad13c9f2d104b392f89f34f0dc4"
147
+ },
136
148
  "storage/TraceContentBudget.d.ts": {
137
149
  "size": 159,
138
150
  "sha256": "d899a615e6cf2a5eea51f6200347cb81fbaae11979b801859f57135083aaf85b"
139
151
  },
140
152
  "storage/TraceContentBudget.js": {
141
- "size": 4022,
142
- "sha256": "4b1d4f0ad7da15caeaa1f9fe8b4edcd3000b0000c5bdc270601cd6db2eedce2e"
153
+ "size": 5868,
154
+ "sha256": "020597ed8a44def06677b8f0ae3947f44cb8c53d03803a2cf214a831858288a7"
143
155
  },
144
156
  "storage/TraceContentRedaction.d.ts": {
145
157
  "size": 207,
@@ -182,8 +194,8 @@
182
194
  "sha256": "d916e8e3abb1b1087f6b184851b0e6265e53380d7857b008e745d566aad15d44"
183
195
  },
184
196
  "types.d.ts": {
185
- "size": 8448,
186
- "sha256": "de994120c04696e08afb428cff27a99fb2918f5c4277b8190b8fb3d36d139f0d"
197
+ "size": 8919,
198
+ "sha256": "4ff6c7c067fa9d16535e4356181a73325977c6813a13983ae9edb17c5fa3c466"
187
199
  },
188
200
  "types.js": {
189
201
  "size": 696,
@@ -210,8 +222,8 @@
210
222
  "sha256": "a7809e98f76b4e326262c26b0e43e278b1c50ac0b35fee145e3e6006357dc700"
211
223
  },
212
224
  "utils/entryFilter.js": {
213
- "size": 3279,
214
- "sha256": "0ce8c5955411194447a16bd79c2f0a33ba60f63b2bd010ac33772718f02f0124"
225
+ "size": 4123,
226
+ "sha256": "de2690c6e5852969083387b469fd2e12f10470c6eee81120154437cd8d8f3555"
215
227
  },
216
228
  "utils/familyHash.d.ts": {
217
229
  "size": 60,
@@ -310,20 +322,20 @@
310
322
  "sha256": "f318cdeec954ce0bba97be1dc11a6dff935b081e6b6a417c614be1934fa47f04"
311
323
  },
312
324
  "watchers/HttpClientWatcher.d.ts": {
313
- "size": 333,
314
- "sha256": "08ab7e213c489ecc4fdd3166d7a121b9a5220ff9cbae9841d4787d9a804e11ce"
325
+ "size": 341,
326
+ "sha256": "7e20bd9240de2165def5d90ec529e4da4e0a7302bc855bd2fb873f8b71d0182f"
315
327
  },
316
328
  "watchers/HttpClientWatcher.js": {
317
- "size": 2414,
318
- "sha256": "817c74e7a89bcd0c53c0344d713b422e0c9e51ec65e7b6c97f0c486116dda7a5"
329
+ "size": 5245,
330
+ "sha256": "dcbc10ac6fd583a009bf37c6787738765f5b54bd2a15a1cd6582d0bb2bbb1207"
319
331
  },
320
332
  "watchers/HttpWatcher.d.ts": {
321
333
  "size": 96,
322
334
  "sha256": "ce9a95a670f755193fd74ce721dbfa4b30f20c879a6566ebb35229b3b2435429"
323
335
  },
324
336
  "watchers/HttpWatcher.js": {
325
- "size": 5916,
326
- "sha256": "9b3fed08fd11f8a2bfe1f667293af437b00f11dcc66bfb545b2f77925394e611"
337
+ "size": 6165,
338
+ "sha256": "aa18bc520f5f17a73aa12b71e57d825f5fc75cddb909ebdb222c0ce03291685d"
327
339
  },
328
340
  "watchers/JobWatcher.d.ts": {
329
341
  "size": 441,
@@ -354,16 +366,16 @@
354
366
  "sha256": "5a28b472c835bd0f79ec9d3670516544bc5b6da9f1f1bed5159be0cba39304a1"
355
367
  },
356
368
  "watchers/MiddlewareWatcher.js": {
357
- "size": 1133,
358
- "sha256": "90cb56a6266e5793a6f1b93b769ad74caf83bef1cf80fa4cc67a87185c401d19"
369
+ "size": 1399,
370
+ "sha256": "54c7c2c7b1cbeaf3987a7e888b3494c12e8848bfb9d07be8be35934bd6a7c63d"
359
371
  },
360
372
  "watchers/ModelWatcher.d.ts": {
361
373
  "size": 285,
362
374
  "sha256": "4c05112af855a92b3f3f97c6b61bf1b07444bcb7d5a17f3e0ae19da4a85faf1c"
363
375
  },
364
376
  "watchers/ModelWatcher.js": {
365
- "size": 1130,
366
- "sha256": "de3d1e379c7b1289167fe0b1dbf2aa5a54137b841df17c8d397ee656d9ce37fd"
377
+ "size": 1381,
378
+ "sha256": "0ead883fad311762f8310a3748d4859d2913ad92c8fd0db48b7070fe4b5e9d70"
367
379
  },
368
380
  "watchers/NotificationWatcher.d.ts": {
369
381
  "size": 274,
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'];
@@ -215,6 +220,19 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
215
220
  return ' method-other';
216
221
  };
217
222
 
223
+ const normalizeMethodLabel = (value) => {
224
+ const method = String(value || '').trim().toUpperCase();
225
+ if (method === '') return 'Request';
226
+ return method.charAt(0) + method.slice(1).toLowerCase();
227
+ };
228
+
229
+ const entryTypeLabel = (entry) => {
230
+ if (entry && entry.type === 'request') {
231
+ return normalizeMethodLabel(entry.content && entry.content.method);
232
+ }
233
+ return String(entry && entry.type || '');
234
+ };
235
+
218
236
  const typeClass = (entryOrType, maybeEntry) => {
219
237
  const entry = maybeEntry || (typeof entryOrType === 'object' ? entryOrType : null);
220
238
  const type = entry && entry.type ? entry.type : entryOrType;
@@ -427,7 +445,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
427
445
 
428
446
  const entrySummaryText = (entry) => {
429
447
  const content = entry && entry.content ? entry.content : {};
430
- if (entry.type === 'request') return [content.responseStatus || '', content.method || '', content.uri || ''].filter(Boolean).join(' ');
448
+ if (entry.type === 'request') return [content.responseStatus || '', content.uri || ''].filter(Boolean).join(' ');
431
449
  if (entry.type === 'query') return String(content.sql || '').slice(0, 160);
432
450
  if (entry.type === 'exception') return [content.class || '', content.message || ''].filter(Boolean).join(': ');
433
451
  if (entry.type === 'log') return '[' + String(content.level || 'log') + '] ' + String(content.message || '').slice(0, 160);
@@ -624,16 +642,17 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
624
642
  const renderTraceItems = (entries, options = {}) => {
625
643
  if (entries.length === 0) return '<p class="trace-note">No related entries captured.</p>';
626
644
 
627
- const collapsible = options.collapsible === true;
628
- const isInitiallyOpen = options.collapsed !== true;
645
+ const collapsible = options.collapsible !== false;
646
+ const isInitiallyOpen = options.collapsed === false;
629
647
 
630
648
  return '<div class="trace-panel">' + entries.map((entry) => {
631
649
  if (collapsible) {
632
650
  return [
633
651
  '<details class="trace-item trace-disclosure"' + (isInitiallyOpen ? ' open' : '') + '>',
634
652
  '<summary class="trace-item-head trace-summary">',
653
+ '<span class="trace-summary-icon">' + DISCLOSURE_ICON + '</span>',
635
654
  '<span class="trace-summary-main">',
636
- '<span><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span></span>',
655
+ '<span><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></span>',
637
656
  '<span class="trace-summary-copy">' + entrySummaryInlineHtml(entry) + '</span>',
638
657
  '</span>',
639
658
  '<span class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></span>',
@@ -650,7 +669,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
650
669
  '<section class="trace-item">',
651
670
  '<div class="trace-item-head">',
652
671
  '<div>',
653
- '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span>',
672
+ '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span>',
654
673
  '</div>',
655
674
  '<div class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div>',
656
675
  '</div>',
@@ -673,14 +692,16 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
673
692
  { id: 'headers', label: 'Headers' },
674
693
  { id: 'response', label: 'Response' },
675
694
  { id: 'queries', label: 'Queries', count: batchEntriesByType('query').length },
695
+ { id: 'middleware', label: 'Middleware', count: batchEntriesByType('middleware').length },
696
+ { id: 'models', label: 'Models', count: batchEntriesByType('model').length },
676
697
  { id: 'logs', label: 'Logs', count: batchEntriesByType('log').length },
677
698
  { id: 'exceptions', label: 'Exceptions', count: batchEntriesByType('exception').length },
678
699
  { id: 'http', label: 'HTTP', count: batchEntriesByType('client_request').length },
679
700
  { id: 'cache', label: 'Cache', count: batchEntriesByType('cache').length },
680
- { id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type)).length }
701
+ { id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','middleware','model','log','exception','client_request','cache'].includes(item.type)).length }
681
702
  ];
682
703
  const currentTab = traceTabs.some((tab) => tab.id === state.detailTab) ? state.detailTab : 'summary';
683
- const otherEntries = batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type));
704
+ const otherEntries = batchEntries().filter((item) => !['request','query','middleware','model','log','exception','client_request','cache'].includes(item.type));
684
705
  const panels = {
685
706
  summary: [
686
707
  '<div class="detail-grid">',
@@ -699,12 +720,17 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
699
720
  renderMetricBox('Tags', [
700
721
  { label: 'Values', value: tagsHtml(entry.tags) || '<span class="activity-time">-</span>' }
701
722
  ]),
723
+ renderMetricBox('Route middleware', [
724
+ { label: 'Attached', value: escapeHtml(Array.isArray(content.middleware) && content.middleware.length > 0 ? content.middleware.join(', ') : 'None') }
725
+ ]),
702
726
  '</div>'
703
727
  ].join(''),
704
728
  payload: detailJson(content.payload || {}, 'Payload Json'),
705
729
  headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
706
730
  response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
707
- queries: renderTraceItems(batchEntriesByType('query'), { collapsible: true, collapsed: true }),
731
+ queries: renderTraceItems(batchEntriesByType('query')),
732
+ middleware: renderTraceItems(batchEntriesByType('middleware')),
733
+ models: renderTraceItems(batchEntriesByType('model')),
708
734
  logs: renderTraceItems(batchEntriesByType('log')),
709
735
  exceptions: renderTraceItems(batchEntriesByType('exception')),
710
736
  http: renderTraceItems(batchEntriesByType('client_request')),
@@ -716,8 +742,8 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
716
742
  '<span class="back-link" data-action="close-detail"><- Back to entries</span>',
717
743
  '<section class="panel detail-card">',
718
744
  '<div>' + (entry.type === 'request'
719
- ? '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> <span class="' + typeClass(entry) + '">' + escapeHtml(content.responseStatus || '') + '</span> <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
720
- : '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
745
+ ? '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> <span class="' + typeClass(entry) + '">' + escapeHtml(content.responseStatus || '') + '</span> <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
746
+ : '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
721
747
  '<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
722
748
  '<div class="trace-tabs">',
723
749
  traceTabs.map((tab) => '<button type="button" class="trace-tab' + (tab.id === currentTab ? ' active' : '') + '" data-action="detail-tab" data-tab="' + escapeHtml(tab.id) + '">' + escapeHtml(tab.label) + (tab.count !== undefined ? ' (' + escapeHtml(tab.count) + ')' : '') + '</button>').join(''),
@@ -743,10 +769,10 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
743
769
  const recentRows = recent.data || [];
744
770
  const recentTable = recentRows.length === 0
745
771
  ? '<div class="empty">No trace entries recorded.</div>'
746
- : '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Summary</th><th>Tags</th><th>Duration</th><th>Happened</th></tr></thead><tbody>' + recentRows.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('') + '</tbody></table></div>';
772
+ : '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Summary</th><th>Tags</th><th>Duration</th><th>Happened</th></tr></thead><tbody>' + recentRows.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('') + '</tbody></table></div>';
747
773
  const activityList = recentRows.length === 0
748
774
  ? '<div class="empty">No recent activity.</div>'
749
- : '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span>' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div><div class="activity-summary">' + escapeHtml(entrySummaryText(entry)) + '</div></li>').join('') + '</ul>';
775
+ : '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span>' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div><div class="activity-summary">' + escapeHtml(entrySummaryText(entry)) + '</div></li>').join('') + '</ul>';
750
776
 
751
777
  main.innerHTML = [
752
778
  statsCardsHtml(stats),
@@ -790,7 +816,7 @@ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
790
816
  const total = Number(response.total || 0);
791
817
  const perPage = Number(response.perPage || 50);
792
818
  const totalPages = Math.max(1, Math.ceil(total / perPage));
793
- const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="mono">' + batchSnippet(entry.batchId) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('');
819
+ const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="mono">' + batchSnippet(entry.batchId) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('');
794
820
 
795
821
  main.innerHTML = [
796
822
  '<section class="panel">',
@@ -1055,6 +1081,7 @@ const buildDashboardHtml = (basePath, projectName) => {
1055
1081
  .replace('__TRACE_SUN_ICON__', JSON.stringify(SUN_ICON))
1056
1082
  .replace('__TRACE_MOON_ICON__', JSON.stringify(MOON_ICON))
1057
1083
  .replace('__TRACE_COPY_ICON__', JSON.stringify(COPY_ICON))
1084
+ .replace('__TRACE_DISCLOSURE_ICON__', JSON.stringify(DISCLOSURE_ICON))
1058
1085
  .replace('__TRACE_JSON_REGEX__', JSON.stringify(JSON_HIGHLIGHT_PATTERN))
1059
1086
  .replace('__TRACE_SQL_REGEX__', JSON.stringify(SQL_HIGHLIGHT_PATTERN))
1060
1087
  .replace('__TRACE_BASE_PATH_LABEL__', basePath)
@@ -0,0 +1,13 @@
1
+ /**
2
+ * TraceStorage — sealed namespace wrapping the D1/SQLite driver.
3
+ * Resolves the correct IDatabase from the app config, then delegates all
4
+ * read/write operations to the trace storage facade.
5
+ */
6
+ import type { IDatabase } from '@zintrust/core';
7
+ import type { ITraceStorage } from '../types';
8
+ export declare const TraceStorage: Readonly<{
9
+ resolveStorage: (db: IDatabase) => ITraceStorage;
10
+ reset: () => void;
11
+ familyHash: (input: string) => string;
12
+ }>;
13
+ export { type ITraceStorage } from '../types';
@@ -0,0 +1,195 @@
1
+ import { familyHash } from '../utils/familyHash.js';
2
+ const TABLE_ENTRIES = 'zin_trace_entries';
3
+ const TABLE_TAGS = 'zin_trace_entries_tags';
4
+ const TABLE_MONITORING = 'zin_trace_monitoring';
5
+ const generateUuid = () => crypto.randomUUID();
6
+ const rowToEntry = (row, tags) => ({
7
+ uuid: row.uuid,
8
+ batchId: row.batch_id,
9
+ familyHash: row.family_hash ?? undefined,
10
+ type: row.type,
11
+ content: JSON.parse(row.content),
12
+ tags,
13
+ isLatest: Boolean(row.is_latest),
14
+ createdAt: row.created_at,
15
+ });
16
+ const insertTags = async (db, uuid, tags) => {
17
+ if (tags.length === 0)
18
+ return;
19
+ await Promise.all(tags.map(async (tag) => {
20
+ await db.execute(`INSERT OR IGNORE INTO ${TABLE_TAGS} (entry_uuid, tag) VALUES (?, ?)`, [
21
+ uuid,
22
+ tag,
23
+ ]);
24
+ }));
25
+ };
26
+ const buildEntryFilters = (opts) => {
27
+ const conditions = [];
28
+ const params = [];
29
+ if (opts.type) {
30
+ conditions.push('e.type = ?');
31
+ params.push(opts.type);
32
+ }
33
+ if (opts.batchId) {
34
+ conditions.push('e.batch_id = ?');
35
+ params.push(opts.batchId);
36
+ }
37
+ if (opts.from) {
38
+ conditions.push('e.created_at >= ?');
39
+ params.push(opts.from);
40
+ }
41
+ if (opts.to) {
42
+ conditions.push('e.created_at <= ?');
43
+ params.push(opts.to);
44
+ }
45
+ let joinClause = '';
46
+ if (opts.tag) {
47
+ joinClause = `INNER JOIN ${TABLE_TAGS} t ON t.entry_uuid = e.uuid AND t.tag = ?`;
48
+ params.unshift(opts.tag);
49
+ }
50
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
51
+ const countParams = opts.tag ? [opts.tag, ...params.slice(1)] : [...params];
52
+ return { joinClause, whereClause, params, countParams };
53
+ };
54
+ const loadTagsByUuid = async (db, uuids) => {
55
+ const tagsByUuid = new Map();
56
+ if (uuids.length === 0)
57
+ return tagsByUuid;
58
+ const tagRows = (await db.query(`SELECT entry_uuid, tag FROM ${TABLE_TAGS} WHERE entry_uuid IN (${uuids.map(() => '?').join(',')})`, uuids));
59
+ for (const tagRow of tagRows) {
60
+ const tags = tagsByUuid.get(tagRow.entry_uuid) ?? [];
61
+ tags.push(tagRow.tag);
62
+ tagsByUuid.set(tagRow.entry_uuid, tags);
63
+ }
64
+ return tagsByUuid;
65
+ };
66
+ // The storage facade intentionally groups related DB operations in one factory.
67
+ // eslint-disable-next-line max-lines-per-function
68
+ const createStorage = (db) => {
69
+ const writeEntry = async (entry) => {
70
+ const uuid = entry.uuid || generateUuid();
71
+ await db.execute(`INSERT INTO ${TABLE_ENTRIES} (uuid, batch_id, family_hash, type, content, is_latest, created_at)
72
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
73
+ uuid,
74
+ entry.batchId,
75
+ entry.familyHash ?? null,
76
+ entry.type,
77
+ JSON.stringify(entry.content),
78
+ entry.isLatest ? 1 : 0,
79
+ entry.createdAt,
80
+ ]);
81
+ await insertTags(db, uuid, entry.tags);
82
+ };
83
+ const updateEntry = async (uuid, patch) => {
84
+ const sets = [];
85
+ const params = [];
86
+ if (patch.content !== undefined) {
87
+ sets.push('content = ?');
88
+ params.push(JSON.stringify(patch.content));
89
+ }
90
+ if (patch.isLatest !== undefined) {
91
+ sets.push('is_latest = ?');
92
+ params.push(patch.isLatest ? 1 : 0);
93
+ }
94
+ if (sets.length === 0)
95
+ return;
96
+ params.push(uuid);
97
+ await db.execute(`UPDATE ${TABLE_ENTRIES} SET ${sets.join(', ')} WHERE uuid = ?`, params);
98
+ };
99
+ const markFamilyStale = async (hash, exceptUuid) => {
100
+ await db.execute(`UPDATE ${TABLE_ENTRIES} SET is_latest = 0
101
+ WHERE family_hash = ? AND uuid != ? AND is_latest = 1`, [hash, exceptUuid]);
102
+ };
103
+ const queryEntries = async (opts) => {
104
+ const page = opts.page ?? 1;
105
+ const perPage = opts.perPage ?? 50;
106
+ const offset = (page - 1) * perPage;
107
+ const { joinClause, whereClause, params, countParams } = buildEntryFilters(opts);
108
+ const countResult = (await db.queryOne(`SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES} e ${joinClause} ${whereClause}`, countParams));
109
+ const total = countResult?.cnt ?? 0;
110
+ const rows = (await db.query(`SELECT e.id, e.uuid, e.batch_id, e.family_hash, e.type, e.content, e.is_latest, e.created_at
111
+ FROM ${TABLE_ENTRIES} e ${joinClause} ${whereClause}
112
+ ORDER BY e.created_at DESC, e.id DESC
113
+ LIMIT ? OFFSET ?`, [...params, perPage, offset]));
114
+ const tagsByUuid = await loadTagsByUuid(db, rows.map((row) => row.uuid));
115
+ return {
116
+ data: rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? [])),
117
+ total,
118
+ };
119
+ };
120
+ const getEntry = async (uuid) => {
121
+ const row = (await db.queryOne(`SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
122
+ FROM ${TABLE_ENTRIES}
123
+ WHERE uuid = ?`, [uuid]));
124
+ if (!row)
125
+ return null;
126
+ const tags = (await db.query(`SELECT tag FROM ${TABLE_TAGS} WHERE entry_uuid = ?`, [
127
+ uuid,
128
+ ]));
129
+ return rowToEntry(row, tags.map((tag) => tag.tag));
130
+ };
131
+ const getBatch = async (batchId) => {
132
+ const rows = (await db.query(`SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
133
+ FROM ${TABLE_ENTRIES}
134
+ WHERE batch_id = ?
135
+ ORDER BY created_at ASC, id ASC`, [batchId]));
136
+ if (rows.length === 0)
137
+ return [];
138
+ const tagsByUuid = await loadTagsByUuid(db, rows.map((row) => row.uuid));
139
+ return rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? []));
140
+ };
141
+ const prune = async (olderThanMs, keepExceptions = false) => {
142
+ const countResult = (await db.queryOne(`SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES}
143
+ WHERE created_at < ?
144
+ ${keepExceptions ? "AND type != 'exception'" : ''}`, [olderThanMs]));
145
+ const deleted = countResult?.cnt ?? 0;
146
+ if (deleted === 0)
147
+ return 0;
148
+ await db.execute(`DELETE FROM ${TABLE_ENTRIES}
149
+ WHERE created_at < ?
150
+ ${keepExceptions ? "AND type != 'exception'" : ''}`, [olderThanMs]);
151
+ return deleted;
152
+ };
153
+ const clear = async () => {
154
+ await db.execute(`DELETE FROM ${TABLE_ENTRIES}`, []);
155
+ };
156
+ const getMonitoring = async () => {
157
+ const rows = (await db.query(`SELECT tag FROM ${TABLE_MONITORING}`, []));
158
+ return rows.map((row) => row.tag);
159
+ };
160
+ const addMonitoring = async (tag) => {
161
+ await db.execute(`INSERT OR IGNORE INTO ${TABLE_MONITORING} (tag) VALUES (?)`, [tag]);
162
+ };
163
+ const removeMonitoring = async (tag) => {
164
+ await db.execute(`DELETE FROM ${TABLE_MONITORING} WHERE tag = ?`, [tag]);
165
+ };
166
+ const stats = async () => {
167
+ const rows = (await db.query(`SELECT type, COUNT(*) as cnt FROM ${TABLE_ENTRIES} GROUP BY type`, []));
168
+ const output = {};
169
+ for (const row of rows) {
170
+ output[row.type] = row.cnt;
171
+ }
172
+ return output;
173
+ };
174
+ return {
175
+ writeEntry,
176
+ updateEntry,
177
+ markFamilyStale,
178
+ queryEntries,
179
+ getEntry,
180
+ getBatch,
181
+ prune,
182
+ clear,
183
+ getMonitoring,
184
+ addMonitoring,
185
+ removeMonitoring,
186
+ stats,
187
+ };
188
+ };
189
+ const resolveStorage = (db) => {
190
+ return createStorage(db);
191
+ };
192
+ const reset = () => {
193
+ return;
194
+ };
195
+ export const TraceStorage = Object.freeze({ resolveStorage, reset, familyHash });