@zintrust/trace 0.4.84 → 0.4.92

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.
@@ -76,11 +76,11 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
76
76
  .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}
77
77
  .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)}
78
78
  .table-wrap{overflow:auto;padding:0 12px 12px}table{width:100%;border-collapse:separate;border-spacing:0;min-width:880px}th{padding:14px;color:var(--muted);font-size:.74rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;text-align:left;border-bottom:1px solid var(--line)}td{padding:15px 14px;border-bottom:1px solid var(--line);vertical-align:top}.row-button{cursor:pointer}.row-button:hover td{background:rgba(56,189,248,.05)}.summary{font-size:.93rem;font-weight:700;line-height:1.4;color:var(--text)}.summary-sub{margin-top:6px;color:var(--muted);font-size:.82rem;line-height:1.4}.mono{font-family:var(--mono)}.empty{padding:44px 24px;color:var(--muted);line-height:1.65;text-align:center}.pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 24px 24px;color:var(--muted);flex-wrap:wrap}.pagination-controls{display:flex;gap:8px}.pagination button{height:40px;min-width:92px;padding:0 14px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer}.pagination button:disabled{opacity:.45;cursor:not-allowed}
79
- .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}.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}.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}
79
+ .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}
80
80
  .tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18);text-decoration:none}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-request.method-get{background:rgba(34,197,94,.16);color:#bbf7d0}.pill-request.method-post{background:rgba(59,130,246,.16);color:#bfdbfe}.pill-request.method-other{background:rgba(245,158,11,.16);color:#fde68a}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-request.method-get{color:#166534}html[data-theme='light'] .pill-request.method-post{color:#1d4ed8}html[data-theme='light'] .pill-request.method-other{color:#92400e}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}
81
81
  .monitoring-wrap{padding:0 24px 24px}.tag-list{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.tag-item{display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:999px;border:1px solid var(--line);background:var(--surface-strong)}.tag-remove{border:none;background:rgba(239,68,68,.14);color:var(--danger);border-radius:999px;width:24px;height:24px;cursor:pointer;font-size:1rem;line-height:1}.helper-text{color:var(--muted);line-height:1.6}
82
82
  .duration-chip{display:inline-flex;align-items:center;padding:5px 9px;border-radius:999px;border:1px solid transparent;font-size:.8rem;font-weight:700;color:var(--text);white-space:nowrap}.duration-chip.vfast{background:rgba(34,197,94,.14);border-color:rgba(34,197,94,.28);color:#bbf7d0}.duration-chip.fast{background:rgba(56,189,248,.12);border-color:rgba(56,189,248,.24);color:#bae6fd}.duration-chip.slow{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#fde68a}.duration-chip.vslow{background:rgba(239,68,68,.14);border-color:rgba(239,68,68,.24);color:#fecaca}html[data-theme='light'] .duration-chip.vfast{color:#166534}html[data-theme='light'] .duration-chip.fast{color:#1d4ed8}html[data-theme='light'] .duration-chip.slow{color:#92400e}html[data-theme='light'] .duration-chip.vslow{color:#b91c1c}
83
- .code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block code{font-family:inherit}.html-preview-wrap{padding:14px;background:var(--surface-strong);border-top:1px solid var(--line)}.html-preview{display:block;width:100%;min-height:320px;border:1px solid var(--line);border-radius:14px;background:#fff}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
83
+ .code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block.wrap{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.code-block code{font-family:inherit}.html-preview-wrap{padding:14px;background:var(--surface-strong);border-top:1px solid var(--line)}.html-preview{display:block;width:100%;min-height:320px;border:1px solid var(--line);border-radius:14px;background:#fff}.inline-collapse{margin:0;border-top:1px solid var(--line);background:var(--surface-strong)}.inline-collapse summary{cursor:pointer;list-style:none;padding:14px 16px;font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.inline-collapse summary::-webkit-details-marker{display:none}.inline-collapse[open] summary{border-bottom:1px solid var(--line)}.inline-collapse .code-block{border-top:none}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
84
84
  @media (max-width:1120px){.content-grid{grid-template-columns:1fr}}@media (max-width:920px){.layout{grid-template-columns:1fr}.sidebar{position:static;height:auto;border-right:none;border-bottom:1px solid var(--line);padding:20px 16px 18px}.brand-row{padding:0 0 16px}.sidebar-status{margin:0 0 16px}.sidebar-group{padding:0}.main{padding:20px}}@media (max-width:640px){.stats-grid{grid-template-columns:1fr}.detail-card{padding:18px}.toolbar,.section-head,.pagination,.activity-list,.monitoring-wrap{padding-left:18px;padding-right:18px}.table-wrap{padding:0 8px 10px}.brand-row{align-items:stretch;gap:14px;padding:0 0 14px}.brand{width:100%;align-items:flex-start}.brand-copy{min-width:0}.brand-copy h1{font-size:1.18rem;line-height:1.12}.brand-copy p{font-size:.82rem;overflow-wrap:anywhere}.icon-button{align-self:flex-end}.sidebar-status{padding:12px}.nav-button{padding:11px 12px}.nav-title{font-size:.95rem}.nav-meta{font-size:.72rem}}@media (max-width:480px){.brand-row{flex-direction:column}.icon-button{align-self:flex-start}.nav-button{align-items:flex-start;flex-direction:column}.nav-meta{font-size:.7rem}}
85
85
  </style>
86
86
  </head>
@@ -328,8 +328,9 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
328
328
  return id;
329
329
  };
330
330
 
331
- const renderCodeCard = (label, rawText, highlightedHtml, languageClass) => {
331
+ const renderCodeCard = (label, rawText, highlightedHtml, languageClass, options = {}) => {
332
332
  const copyId = registerCopyPayload(rawText);
333
+ const wrapClass = options.wrap === true ? ' wrap' : '';
333
334
  return [
334
335
  '<section class="code-card">',
335
336
  '<div class="code-toolbar">',
@@ -338,19 +339,23 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
338
339
  COPY_ICON,
339
340
  '</button>',
340
341
  '</div>',
341
- '<pre class="code-block ' + escapeHtml(languageClass) + '"><code>' + highlightedHtml + '</code></pre>',
342
+ '<pre class="code-block ' + escapeHtml(languageClass) + wrapClass + '"><code>' + highlightedHtml + '</code></pre>',
342
343
  '</section>'
343
344
  ].join('');
344
345
  };
345
346
 
346
347
  const renderTextCard = (label, value) => {
347
348
  const source = String(value ?? '');
348
- return renderCodeCard(label, source, escapeHtml(source), 'language-text');
349
+ return renderCodeCard(label, source, escapeHtml(source), 'language-text', { wrap: true });
349
350
  };
350
351
 
351
- const renderHtmlPreview = (label, html) => {
352
+ const renderHtmlPreview = (label, html, options = {}) => {
352
353
  const source = String(html ?? '');
353
354
  const copyId = registerCopyPayload(source);
355
+ const rawHtmlBlock = '<pre class="code-block language-html wrap"><code>' + escapeHtml(source) + '</code></pre>';
356
+ const rawHtmlSection = options.collapseSource === true
357
+ ? '<details class="inline-collapse"><summary>View raw HTML source</summary>' + rawHtmlBlock + '</details>'
358
+ : rawHtmlBlock;
354
359
  return [
355
360
  '<section class="code-card">',
356
361
  '<div class="code-toolbar">',
@@ -359,8 +364,8 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
359
364
  COPY_ICON,
360
365
  '</button>',
361
366
  '</div>',
362
- '<pre class="code-block language-html"><code>' + escapeHtml(source) + '</code></pre>',
363
367
  '<div class="html-preview-wrap"><iframe class="html-preview" sandbox="allow-same-origin" srcdoc="' + escapeHtml(source) + '"></iframe></div>',
368
+ rawHtmlSection,
364
369
  '</section>'
365
370
  ].join('');
366
371
  };
@@ -384,7 +389,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
384
389
  }
385
390
 
386
391
  output += escapeHtml(source.slice(lastIndex));
387
- return renderCodeCard(label, source, output, 'language-json');
392
+ return renderCodeCard(label, source, output, 'language-json', { wrap: true });
388
393
  };
389
394
 
390
395
  const highlightSql = (sql) => {
@@ -419,7 +424,9 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
419
424
  const renderPayload = (label, value) => {
420
425
  if (value === undefined) return '<p class="trace-note">No ' + escapeHtml(label.toLowerCase()) + ' was captured.</p>';
421
426
  if (typeof value === 'string') {
422
- return looksLikeHtml(value) ? renderHtmlPreview(label, value) : renderTextCard(label, value);
427
+ return looksLikeHtml(value)
428
+ ? renderHtmlPreview(label, value, { collapseSource: true })
429
+ : renderTextCard(label, value);
423
430
  }
424
431
  return detailJson(value, label);
425
432
  };
@@ -459,6 +466,17 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
459
466
  return '<div class="summary">' + summary + '</div><div class="summary-sub">' + escapeHtml(secondary) + '</div>';
460
467
  };
461
468
 
469
+ const entrySummaryInlineHtml = (entry) => {
470
+ const summary = escapeHtml(entrySummaryText(entry) || 'No summary available');
471
+ const secondary = [
472
+ entry.type === 'request' ? 'Incoming request' : '',
473
+ entry.type === 'query' ? 'Database query' : '',
474
+ entry.type === 'exception' ? 'Unhandled error' : '',
475
+ entry.type === 'client_request' ? 'Outbound HTTP call' : ''
476
+ ].find(Boolean) || 'Trace record';
477
+ return '<span class="summary">' + summary + '</span><span class="summary-sub">' + escapeHtml(secondary) + '</span>';
478
+ };
479
+
462
480
  const renderMetricBox = (title, items) => {
463
481
  return [
464
482
  '<section class="detail-box">',
@@ -583,8 +601,8 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
583
601
  ]),
584
602
  '</div>',
585
603
  '<div class="detail-stack">',
586
- renderPayload('Mail Text', content.text),
587
604
  renderPayload('Mail Html', content.html),
605
+ renderPayload('Mail Text', content.text),
588
606
  '</div>'
589
607
  ].join('');
590
608
  }
@@ -609,10 +627,31 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
609
627
  return detailJson(content);
610
628
  };
611
629
 
612
- const renderTraceItems = (entries) => {
630
+ const renderTraceItems = (entries, options = {}) => {
613
631
  if (entries.length === 0) return '<p class="trace-note">No related entries captured.</p>';
614
632
 
633
+ const collapsible = options.collapsible === true;
634
+ const isInitiallyOpen = options.collapsed !== true;
635
+
615
636
  return '<div class="trace-panel">' + entries.map((entry) => {
637
+ if (collapsible) {
638
+ return [
639
+ '<details class="trace-item trace-disclosure"' + (isInitiallyOpen ? ' open' : '') + '>',
640
+ '<summary class="trace-item-head trace-summary">',
641
+ '<span class="trace-summary-main">',
642
+ '<span><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span></span>',
643
+ '<span class="trace-summary-copy">' + entrySummaryInlineHtml(entry) + '</span>',
644
+ '</span>',
645
+ '<span class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></span>',
646
+ '</summary>',
647
+ '<div class="trace-disclosure-body">',
648
+ '<div>' + tagsHtml(entry.tags) + '</div>',
649
+ renderEntryBody(entry),
650
+ '</div>',
651
+ '</details>'
652
+ ].join('');
653
+ }
654
+
616
655
  return [
617
656
  '<section class="trace-item">',
618
657
  '<div class="trace-item-head">',
@@ -671,7 +710,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
671
710
  payload: detailJson(content.payload || {}, 'Payload Json'),
672
711
  headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
673
712
  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>',
674
- queries: renderTraceItems(batchEntriesByType('query')),
713
+ queries: renderTraceItems(batchEntriesByType('query'), { collapsible: true, collapsed: true }),
675
714
  logs: renderTraceItems(batchEntriesByType('log')),
676
715
  exceptions: renderTraceItems(batchEntriesByType('exception')),
677
716
  http: renderTraceItems(batchEntriesByType('client_request')),
package/src/register.ts CHANGED
@@ -64,6 +64,9 @@ type CoreApi = {
64
64
  Logger?: {
65
65
  warn(message: string, context?: Record<string, unknown>): void;
66
66
  };
67
+ ErrorFactory?: {
68
+ createConfigError(message: string, details?: unknown): Error;
69
+ };
67
70
  StartupConfigFile?: {
68
71
  Trace?: string;
69
72
  };
@@ -72,6 +75,14 @@ type CoreApi = {
72
75
  };
73
76
  };
74
77
 
78
+ type CoreDatabase = import('@zintrust/core').IDatabase;
79
+
80
+ const TRACE_REQUIRED_TABLES = [
81
+ 'zin_trace_entries',
82
+ 'zin_trace_entries_tags',
83
+ 'zin_trace_monitoring',
84
+ ] as const;
85
+
75
86
  type GlobalMiddlewareRegistrarState = {
76
87
  __zintrust_register_global_middleware__?: ITraceWatcherConfig['registerMiddleware'];
77
88
  __zintrust_pending_global_middlewares__?: Array<
@@ -125,7 +136,7 @@ const resolveObservedConnectionName = (
125
136
  return resolveTraceConnectionName(env, configuredObservedConnection);
126
137
  }
127
138
 
128
- const defaultConnectionName = resolveTraceConnectionName(env, undefined);
139
+ const defaultConnectionName = resolveTraceConnectionName(env);
129
140
  if (storageConnectionName !== defaultConnectionName) {
130
141
  return defaultConnectionName;
131
142
  }
@@ -217,6 +228,71 @@ const buildTraceRedactionOverrides = (input: {
217
228
  : undefined;
218
229
  };
219
230
 
231
+ const createTraceConfigError = (coreApi: CoreApi, message: string, details?: unknown): Error => {
232
+ if (coreApi.ErrorFactory?.createConfigError !== undefined) {
233
+ return coreApi.ErrorFactory.createConfigError(message, details);
234
+ }
235
+
236
+ const error = new globalThis.Error(message) as Error & {
237
+ code?: string;
238
+ details?: unknown;
239
+ name?: string;
240
+ statusCode?: number;
241
+ };
242
+ error.name = 'ConfigError';
243
+ error.code = 'CONFIG_ERROR';
244
+ error.statusCode = 500;
245
+ error.details = details;
246
+ return error;
247
+ };
248
+
249
+ function assertTraceConnectionResolved(
250
+ coreApi: CoreApi,
251
+ db: CoreDatabase | undefined,
252
+ params: { connectionName: string; envKey: 'TRACE_DB_CONNECTION' | 'TRACE_QUERY_CONNECTION' }
253
+ ): asserts db is CoreDatabase {
254
+ if (db !== undefined) {
255
+ return;
256
+ }
257
+
258
+ throw createTraceConfigError(
259
+ coreApi,
260
+ `Trace connection "${params.connectionName}" could not be resolved.`,
261
+ {
262
+ connectionName: params.connectionName,
263
+ envKey: params.envKey,
264
+ hint:
265
+ params.envKey === 'TRACE_DB_CONNECTION'
266
+ ? 'Configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.'
267
+ : 'Configure TRACE_QUERY_CONNECTION, or ensure DB_CONNECTION resolves to an existing database connection.',
268
+ }
269
+ );
270
+ }
271
+
272
+ const assertTraceStorageReady = async (
273
+ coreApi: CoreApi,
274
+ db: CoreDatabase,
275
+ connectionName: string
276
+ ): Promise<void> => {
277
+ try {
278
+ await Promise.all(
279
+ TRACE_REQUIRED_TABLES.map(async (table) => {
280
+ await db.queryOne(`SELECT 1 AS ok FROM ${table} LIMIT 1`, []);
281
+ })
282
+ );
283
+ } catch (error) {
284
+ throw createTraceConfigError(
285
+ coreApi,
286
+ `Trace storage connection "${connectionName}" is not ready. Create the database if needed and run \`zin migrate:trace\` before enabling TRACE_ENABLED.`,
287
+ {
288
+ connectionName,
289
+ error,
290
+ requiredTables: [...TRACE_REQUIRED_TABLES],
291
+ }
292
+ );
293
+ }
294
+ };
295
+
220
296
  const core = (await importCore()) as CoreApi;
221
297
  const Env = core.Env;
222
298
  const startupOverrides = resolveTraceStartupOverrides(core);
@@ -292,98 +368,103 @@ if (!traceAlreadyInitialized && Env) {
292
368
  const storageDb = core.useDatabase?.(undefined, resolvedConnectionName);
293
369
  const observedDb = core.useDatabase?.(undefined, resolvedObservedConnectionName);
294
370
 
295
- if (storageDb && observedDb) {
296
- const storage = TraceWriteDiagnostics.wrapStorage(
297
- TraceContentRedaction.wrapStorage(
298
- TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config),
299
- config.redaction
300
- ),
301
- {
302
- connectionName: resolvedConnectionName,
303
- logger: core.Logger,
304
- }
305
- );
306
-
307
- if (core.RequestContext) {
308
- TraceContext.setRequestContextImpl(
309
- core.RequestContext as {
310
- current?: () => unknown;
311
- peek?: () => unknown;
312
- }
313
- );
371
+ assertTraceConnectionResolved(core, storageDb, {
372
+ connectionName: resolvedConnectionName,
373
+ envKey: 'TRACE_DB_CONNECTION',
374
+ });
375
+ assertTraceConnectionResolved(core, observedDb, {
376
+ connectionName: resolvedObservedConnectionName,
377
+ envKey: 'TRACE_QUERY_CONNECTION',
378
+ });
379
+ await assertTraceStorageReady(core, storageDb, resolvedConnectionName);
380
+
381
+ const storage = TraceWriteDiagnostics.wrapStorage(
382
+ TraceContentRedaction.wrapStorage(
383
+ TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config),
384
+ config.redaction
385
+ ),
386
+ {
387
+ connectionName: resolvedConnectionName,
388
+ logger: core.Logger,
314
389
  }
390
+ );
315
391
 
316
- const [
317
- { HttpWatcher },
318
- { QueryWatcher },
319
- { LogWatcher },
320
- { ExceptionWatcher },
321
- { JobWatcher },
322
- { CacheWatcher },
323
- { ScheduleWatcher },
324
- { MailWatcher },
325
- { AuthWatcher },
326
- { EventWatcher },
327
- { ModelWatcher },
328
- { NotificationWatcher },
329
- { RedisWatcher },
330
- { GateWatcher },
331
- { MiddlewareWatcher },
332
- { CommandWatcher },
333
- { BatchWatcher },
334
- { DumpWatcher },
335
- { ViewWatcher },
336
- { HttpClientWatcher },
337
- ] = await Promise.all([
338
- import('./watchers/HttpWatcher'),
339
- import('./watchers/QueryWatcher'),
340
- import('./watchers/LogWatcher'),
341
- import('./watchers/ExceptionWatcher'),
342
- import('./watchers/JobWatcher'),
343
- import('./watchers/CacheWatcher'),
344
- import('./watchers/ScheduleWatcher'),
345
- import('./watchers/MailWatcher'),
346
- import('./watchers/AuthWatcher'),
347
- import('./watchers/EventWatcher'),
348
- import('./watchers/ModelWatcher'),
349
- import('./watchers/NotificationWatcher'),
350
- import('./watchers/RedisWatcher'),
351
- import('./watchers/GateWatcher'),
352
- import('./watchers/MiddlewareWatcher'),
353
- import('./watchers/CommandWatcher'),
354
- import('./watchers/BatchWatcher'),
355
- import('./watchers/DumpWatcher'),
356
- import('./watchers/ViewWatcher'),
357
- import('./watchers/HttpClientWatcher'),
358
- ]);
359
-
360
- const watcherArgs = { storage, config, db: observedDb };
361
-
362
- HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
363
-
364
- QueryWatcher.register(watcherArgs);
365
- LogWatcher.register(watcherArgs);
366
- ExceptionWatcher.register(watcherArgs);
367
- JobWatcher.register(watcherArgs);
368
- CacheWatcher.register(watcherArgs);
369
- ScheduleWatcher.register(watcherArgs);
370
- MailWatcher.register(watcherArgs);
371
- AuthWatcher.register(watcherArgs);
372
- EventWatcher.register(watcherArgs);
373
- ModelWatcher.register(watcherArgs);
374
- NotificationWatcher.register(watcherArgs);
375
- RedisWatcher.register(watcherArgs);
376
- GateWatcher.register(watcherArgs);
377
- MiddlewareWatcher.register(watcherArgs);
378
- CommandWatcher.register(watcherArgs);
379
- BatchWatcher.register(watcherArgs);
380
- DumpWatcher.register(watcherArgs);
381
- ViewWatcher.register(watcherArgs);
382
- HttpClientWatcher.register(watcherArgs);
383
- } else {
384
- // eslint-disable-next-line no-console
385
- console.warn('[trace] Could not resolve database connection - skipping init.');
392
+ if (core.RequestContext) {
393
+ TraceContext.setRequestContextImpl(
394
+ core.RequestContext as {
395
+ current?: () => unknown;
396
+ peek?: () => unknown;
397
+ }
398
+ );
386
399
  }
400
+
401
+ const [
402
+ { HttpWatcher },
403
+ { QueryWatcher },
404
+ { LogWatcher },
405
+ { ExceptionWatcher },
406
+ { JobWatcher },
407
+ { CacheWatcher },
408
+ { ScheduleWatcher },
409
+ { MailWatcher },
410
+ { AuthWatcher },
411
+ { EventWatcher },
412
+ { ModelWatcher },
413
+ { NotificationWatcher },
414
+ { RedisWatcher },
415
+ { GateWatcher },
416
+ { MiddlewareWatcher },
417
+ { CommandWatcher },
418
+ { BatchWatcher },
419
+ { DumpWatcher },
420
+ { ViewWatcher },
421
+ { HttpClientWatcher },
422
+ ] = await Promise.all([
423
+ import('./watchers/HttpWatcher'),
424
+ import('./watchers/QueryWatcher'),
425
+ import('./watchers/LogWatcher'),
426
+ import('./watchers/ExceptionWatcher'),
427
+ import('./watchers/JobWatcher'),
428
+ import('./watchers/CacheWatcher'),
429
+ import('./watchers/ScheduleWatcher'),
430
+ import('./watchers/MailWatcher'),
431
+ import('./watchers/AuthWatcher'),
432
+ import('./watchers/EventWatcher'),
433
+ import('./watchers/ModelWatcher'),
434
+ import('./watchers/NotificationWatcher'),
435
+ import('./watchers/RedisWatcher'),
436
+ import('./watchers/GateWatcher'),
437
+ import('./watchers/MiddlewareWatcher'),
438
+ import('./watchers/CommandWatcher'),
439
+ import('./watchers/BatchWatcher'),
440
+ import('./watchers/DumpWatcher'),
441
+ import('./watchers/ViewWatcher'),
442
+ import('./watchers/HttpClientWatcher'),
443
+ ]);
444
+
445
+ const watcherArgs = { storage, config, db: observedDb };
446
+
447
+ HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
448
+
449
+ QueryWatcher.register(watcherArgs);
450
+ LogWatcher.register(watcherArgs);
451
+ ExceptionWatcher.register(watcherArgs);
452
+ JobWatcher.register(watcherArgs);
453
+ CacheWatcher.register(watcherArgs);
454
+ ScheduleWatcher.register(watcherArgs);
455
+ MailWatcher.register(watcherArgs);
456
+ AuthWatcher.register(watcherArgs);
457
+ EventWatcher.register(watcherArgs);
458
+ ModelWatcher.register(watcherArgs);
459
+ NotificationWatcher.register(watcherArgs);
460
+ RedisWatcher.register(watcherArgs);
461
+ GateWatcher.register(watcherArgs);
462
+ MiddlewareWatcher.register(watcherArgs);
463
+ CommandWatcher.register(watcherArgs);
464
+ BatchWatcher.register(watcherArgs);
465
+ DumpWatcher.register(watcherArgs);
466
+ ViewWatcher.register(watcherArgs);
467
+ HttpClientWatcher.register(watcherArgs);
387
468
  }
388
469
  } else if (!traceAlreadyInitialized) {
389
470
  // Running outside a ZinTrust project - skip init silently.
@@ -21,8 +21,50 @@ const TRACE_INFRASTRUCTURE_LOG_MESSAGES = new Set<string>([
21
21
  '[trace] Trace storage write degraded',
22
22
  ]);
23
23
 
24
- const shouldSkipTraceInfrastructureLog = (message: string): boolean => {
25
- return TRACE_INFRASTRUCTURE_LOG_MESSAGES.has(message.trim());
24
+ const TRACE_STORAGE_TABLE_NAMES = [
25
+ 'zin_trace_entries',
26
+ 'zin_trace_entries_tags',
27
+ 'zin_trace_monitoring',
28
+ ];
29
+
30
+ const isTraceStorageQuery = (sql: string): boolean => {
31
+ const normalized = sql.toLowerCase();
32
+ return TRACE_STORAGE_TABLE_NAMES.some((tableName) => normalized.includes(tableName));
33
+ };
34
+
35
+ const extractSqlFromLog = (
36
+ message: string,
37
+ context?: Record<string, unknown>
38
+ ): string | undefined => {
39
+ const contextSql = context?.['sql'];
40
+ if (typeof contextSql === 'string') return contextSql;
41
+
42
+ const trimmed = message.trim();
43
+ const rawPrefix = 'Raw SQL Query executed:';
44
+ if (trimmed.startsWith(rawPrefix)) {
45
+ const sql = trimmed.slice(rawPrefix.length).trim();
46
+ return sql === '' ? undefined : sql;
47
+ }
48
+
49
+ return undefined;
50
+ };
51
+
52
+ const isTraceStorageQueryLog = (message: string, context?: Record<string, unknown>): boolean => {
53
+ const normalizedMessage = message.trim().toLowerCase();
54
+ if (!normalizedMessage.includes('query executed')) return false;
55
+
56
+ const sql = extractSqlFromLog(message, context);
57
+ return typeof sql === 'string' && isTraceStorageQuery(sql);
58
+ };
59
+
60
+ const shouldSkipTraceInfrastructureLog = (
61
+ message: string,
62
+ context?: Record<string, unknown>
63
+ ): boolean => {
64
+ return (
65
+ TRACE_INFRASTRUCTURE_LOG_MESSAGES.has(message.trim()) ||
66
+ isTraceStorageQueryLog(message, context)
67
+ );
26
68
  };
27
69
 
28
70
  export const LogWatcher: ITraceWatcher = Object.freeze({
@@ -41,7 +83,7 @@ export const LogWatcher: ITraceWatcher = Object.freeze({
41
83
  (level: string, message: string, context?: Record<string, unknown>) => {
42
84
  if ((LEVEL_PRIORITY[level] ?? 0) < minPriority) return;
43
85
  if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes)) return;
44
- if (shouldSkipTraceInfrastructureLog(message)) return;
86
+ if (shouldSkipTraceInfrastructureLog(message, context)) return;
45
87
 
46
88
  const content: LogContent = {
47
89
  level,
@@ -24,7 +24,11 @@ const bindingsInterpolated = (sql: string, params: unknown[]): string => {
24
24
 
25
25
  const isTraceStorageQuery = (sql: string): boolean => {
26
26
  const normalized = sql.toLowerCase();
27
- return normalized.includes('zin_trace_entries') || normalized.includes('zin_trace_monitoring');
27
+ return (
28
+ normalized.includes('zin_trace_entries') ||
29
+ normalized.includes('zin_trace_entries_tags') ||
30
+ normalized.includes('zin_trace_monitoring')
31
+ );
28
32
  };
29
33
 
30
34
  const emit = (query: string, params: unknown[], duration: number, connection = 'default'): void => {
@@ -1,13 +0,0 @@
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';