@zintrust/trace 0.4.76 → 0.4.77

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.
Files changed (45) hide show
  1. package/README.md +101 -15
  2. package/dist/build-manifest.json +78 -38
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.js +123 -4
  5. package/dist/dashboard/ui.js +80 -23
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +5 -0
  8. package/dist/migrations/20260331000001_create_zin_trace_entries_table.js +1 -1
  9. package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.d.ts +10 -0
  10. package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.js +34 -0
  11. package/dist/migrations/index.js +2 -1
  12. package/dist/register.js +107 -9
  13. package/dist/storage/TraceContentRedaction.d.ts +4 -0
  14. package/dist/storage/TraceContentRedaction.js +33 -0
  15. package/dist/storage/TraceEntryFiltering.d.ts +4 -0
  16. package/dist/storage/TraceEntryFiltering.js +13 -0
  17. package/dist/storage/TraceStorage.js +35 -5
  18. package/dist/storage/TraceWriteDiagnostics.d.ts +19 -0
  19. package/dist/storage/TraceWriteDiagnostics.js +98 -0
  20. package/dist/types.d.ts +37 -20
  21. package/dist/utils/entryFilter.d.ts +4 -0
  22. package/dist/utils/entryFilter.js +95 -0
  23. package/dist/utils/redact.d.ts +1 -0
  24. package/dist/utils/redact.js +43 -9
  25. package/dist/watchers/CommandWatcher.js +1 -1
  26. package/dist/watchers/HttpClientWatcher.js +1 -1
  27. package/dist/watchers/HttpWatcher.js +104 -20
  28. package/dist/watchers/LogWatcher.js +1 -0
  29. package/package.json +3 -3
  30. package/src/config.ts +152 -5
  31. package/src/dashboard/routes.ts +6 -2
  32. package/src/dashboard/ui.ts +80 -23
  33. package/src/index.ts +7 -0
  34. package/src/register.ts +137 -10
  35. package/src/storage/TraceContentRedaction.ts +44 -0
  36. package/src/storage/TraceEntryFiltering.ts +14 -0
  37. package/src/storage/TraceStorage.ts +52 -5
  38. package/src/storage/TraceWriteDiagnostics.ts +174 -0
  39. package/src/types.ts +40 -20
  40. package/src/utils/entryFilter.ts +108 -0
  41. package/src/utils/redact.ts +57 -9
  42. package/src/watchers/CommandWatcher.ts +1 -1
  43. package/src/watchers/HttpClientWatcher.ts +1 -1
  44. package/src/watchers/HttpWatcher.ts +132 -21
  45. package/src/watchers/LogWatcher.ts +27 -27
@@ -71,7 +71,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
71
71
  .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)}
72
72
  .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}
73
73
  .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}
74
- .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)}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-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-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}
74
+ .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}
75
75
  .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}
76
76
  .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}
77
77
  .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}.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}
@@ -140,15 +140,27 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
140
140
  monitoring: { title: 'Monitoring tags', subtitle: 'Pinned tags for trace pivots.' }
141
141
  };
142
142
 
143
- let state = {
144
- page: 'overview',
145
- entriesPage: 1,
146
- entriesFilter: { type: '', tag: '', batchId: '' },
147
- detail: null,
148
- detailBatch: null,
149
- detailTab: 'summary'
143
+ const createInitialState = () => {
144
+ const search = new URLSearchParams(window.location.search);
145
+ const page = search.get('page');
146
+ const entriesPage = Number.parseInt(search.get('entriesPage') || '1', 10);
147
+
148
+ return {
149
+ page: page && Object.prototype.hasOwnProperty.call(PAGE_COPY, page) ? page : 'overview',
150
+ entriesPage: Number.isFinite(entriesPage) && entriesPage > 0 ? entriesPage : 1,
151
+ entriesFilter: {
152
+ type: search.get('type') || '',
153
+ tag: search.get('tag') || '',
154
+ batchId: search.get('batchId') || ''
155
+ },
156
+ detail: null,
157
+ detailBatch: null,
158
+ detailTab: 'summary'
159
+ };
150
160
  };
151
161
 
162
+ let state = createInitialState();
163
+
152
164
  let copySequence = 0;
153
165
  const copyPayloads = new Map();
154
166
 
@@ -193,7 +205,19 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
193
205
  return response.json();
194
206
  };
195
207
 
196
- const typeClass = (type) => 'type-pill pill-' + String(type || '').replace(/_/g, '-');
208
+ const requestMethodClass = (entry) => {
209
+ if (!entry || entry.type !== 'request') return '';
210
+ const method = String(entry.content && entry.content.method || '').toUpperCase();
211
+ if (method === 'GET') return ' method-get';
212
+ if (method === 'POST') return ' method-post';
213
+ return ' method-other';
214
+ };
215
+
216
+ const typeClass = (entryOrType, maybeEntry) => {
217
+ const entry = maybeEntry || (typeof entryOrType === 'object' ? entryOrType : null);
218
+ const type = entry && entry.type ? entry.type : entryOrType;
219
+ return 'type-pill pill-' + String(type || '').replace(/_/g, '-') + requestMethodClass(entry);
220
+ };
197
221
 
198
222
  const timeSince = (value) => {
199
223
  const createdAt = Number(value);
@@ -245,11 +269,34 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
245
269
  return '<span class="duration-chip ' + tone + '" title="' + toneLabel + '">' + escapeHtml(formatDuration(duration)) + '</span>';
246
270
  };
247
271
 
272
+ const buildEntriesHref = (params) => {
273
+ const search = new URLSearchParams();
274
+ search.set('page', 'entries');
275
+ if (params.type) search.set('type', String(params.type));
276
+ if (params.tag) search.set('tag', String(params.tag));
277
+ if (params.batchId) search.set('batchId', String(params.batchId));
278
+ return BASE + '?' + search.toString();
279
+ };
280
+
248
281
  const tagsHtml = (tags) => (tags || []).map((tag) => {
249
282
  const css = tag === 'failed' ? 'tag failed' : tag === 'slow' ? 'tag slow' : 'tag';
250
- return '<button type="button" class="' + css + '" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '">' + escapeHtml(tag) + '</button>';
283
+ const href = buildEntriesHref({ tag });
284
+ return '<a class="' + css + '" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '" href="' + escapeHtml(href) + '">' + escapeHtml(tag) + '</a>';
251
285
  }).join('');
252
286
 
287
+ const syncUrl = () => {
288
+ const search = new URLSearchParams();
289
+ if (state.page !== 'overview') search.set('page', state.page);
290
+ if (state.page === 'entries' || state.entriesFilter.type || state.entriesFilter.tag || state.entriesFilter.batchId) {
291
+ if (state.entriesFilter.type) search.set('type', state.entriesFilter.type);
292
+ if (state.entriesFilter.tag) search.set('tag', state.entriesFilter.tag);
293
+ if (state.entriesFilter.batchId) search.set('batchId', state.entriesFilter.batchId);
294
+ if (state.entriesPage > 1) search.set('entriesPage', String(state.entriesPage));
295
+ }
296
+ const nextUrl = search.toString() === '' ? BASE : BASE + '?' + search.toString();
297
+ window.history.replaceState(null, '', nextUrl);
298
+ };
299
+
253
300
  const batchSnippet = (batchId) => {
254
301
  const raw = String(batchId || '');
255
302
  return raw === '' ? '-' : escapeHtml(raw.slice(0, 8));
@@ -469,7 +516,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
469
516
  '<section class="trace-item">',
470
517
  '<div class="trace-item-head">',
471
518
  '<div>',
472
- '<span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span>',
519
+ '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span>',
473
520
  '</div>',
474
521
  '<div class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div>',
475
522
  '</div>',
@@ -495,10 +542,11 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
495
542
  { id: 'logs', label: 'Logs', count: batchEntriesByType('log').length },
496
543
  { id: 'exceptions', label: 'Exceptions', count: batchEntriesByType('exception').length },
497
544
  { id: 'http', label: 'HTTP', count: batchEntriesByType('client_request').length },
498
- { id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','log','exception','client_request'].includes(item.type)).length }
545
+ { id: 'cache', label: 'Cache', count: batchEntriesByType('cache').length },
546
+ { id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type)).length }
499
547
  ];
500
548
  const currentTab = traceTabs.some((tab) => tab.id === state.detailTab) ? state.detailTab : 'summary';
501
- const otherEntries = batchEntries().filter((item) => !['request','query','log','exception','client_request'].includes(item.type));
549
+ const otherEntries = batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type));
502
550
  const panels = {
503
551
  summary: [
504
552
  '<div class="detail-grid">',
@@ -521,18 +569,19 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
521
569
  ].join(''),
522
570
  payload: detailJson(content.payload || {}),
523
571
  headers: '<div class="detail-stack">' + detailJson(content.headers || {}) + detailJson(content.responseHeaders || {}) + '</div>',
524
- 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><p class="trace-note">Response body capture is not wired yet. Status and headers are available.</p>' + detailJson(content.responseHeaders || {}) + '</div>',
572
+ 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)) + detailJson(content.responseHeaders || {}) + '</div>',
525
573
  queries: renderTraceItems(batchEntriesByType('query')),
526
574
  logs: renderTraceItems(batchEntriesByType('log')),
527
575
  exceptions: renderTraceItems(batchEntriesByType('exception')),
528
576
  http: renderTraceItems(batchEntriesByType('client_request')),
577
+ cache: renderTraceItems(batchEntriesByType('cache')),
529
578
  other: renderTraceItems(otherEntries)
530
579
  };
531
580
 
532
581
  main.innerHTML = [
533
582
  '<span class="back-link" data-action="close-detail"><- Back to entries</span>',
534
583
  '<section class="panel detail-card">',
535
- '<div><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
584
+ '<div><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
536
585
  '<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>',
537
586
  '<div class="trace-tabs">',
538
587
  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(''),
@@ -558,10 +607,10 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
558
607
  const recentRows = recent.data || [];
559
608
  const recentTable = recentRows.length === 0
560
609
  ? '<div class="empty">No trace entries recorded.</div>'
561
- : '<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.type) + '">' + 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>';
610
+ : '<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>';
562
611
  const activityList = recentRows.length === 0
563
612
  ? '<div class="empty">No recent activity.</div>'
564
- : '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry.type) + '">' + 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>';
613
+ : '<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>';
565
614
 
566
615
  main.innerHTML = [
567
616
  statsCardsHtml(stats),
@@ -605,7 +654,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
605
654
  const total = Number(response.total || 0);
606
655
  const perPage = Number(response.perPage || 50);
607
656
  const totalPages = Math.max(1, Math.ceil(total / perPage));
608
- const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry.type) + '">' + 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('');
657
+ 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('');
609
658
 
610
659
  main.innerHTML = [
611
660
  '<section class="panel">',
@@ -641,7 +690,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
641
690
  main.innerHTML = [
642
691
  '<span class="back-link" data-action="close-detail"><- Back to entries</span>',
643
692
  '<section class="panel detail-card">',
644
- '<div><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
693
+ '<div><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
645
694
  '<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>',
646
695
  '<div class="detail-stack">',
647
696
  renderEntryBody(entry),
@@ -657,13 +706,14 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
657
706
  const tags = result.tags || [];
658
707
  main.innerHTML = [
659
708
  '<section class="panel">',
660
- '<div class="section-head"><div><h3>Monitoring tags</h3><p>Pinned tags for quick filtering.</p></div></div>',
709
+ '<div class="section-head"><div><h3>Monitoring tags</h3><p>Save the tags you filter by often so they stay one click away.</p></div></div>',
661
710
  '<div class="monitoring-wrap">',
662
711
  '<div class="tag-list">',
663
- tags.length === 0 ? '<span class="helper-text">No tags monitored.</span>' : tags.map((tag) => '<span class="tag-item"><button type="button" class="tag mono" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '">' + escapeHtml(tag) + '</button><button type="button" class="tag-remove" data-action="remove-tag" data-tag="' + escapeHtml(tag) + '">x</button></span>').join(''),
712
+ tags.length === 0 ? '<span class="helper-text">No tags monitored yet. Add tags like auth, checkout, queue:emails, or nightly-sync to pin them here.</span>' : tags.map((tag) => '<span class="tag-item"><a class="tag mono" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '" href="' + escapeHtml(buildEntriesHref({ tag })) + '">' + escapeHtml(tag) + '</a><button type="button" class="tag-remove" data-action="remove-tag" data-tag="' + escapeHtml(tag) + '">x</button></span>').join(''),
664
713
  '</div>',
714
+ '<p class="helper-text">Click a saved tag to filter the entries list by that exact tag. Because each tag is a real link, you can also open it in a new tab. Removing a saved tag only removes the shortcut here; it does not delete any trace entries.</p>',
665
715
  '<div class="toolbar" style="padding:0;margin-top:8px">',
666
- '<input id="new-tag" class="control" type="text" placeholder="Add tag">',
716
+ '<input id="new-tag" class="control" type="text" placeholder="Add tag, for example checkout">',
667
717
  '<button type="button" class="btn btn-primary" data-action="add-tag">Add tag</button>',
668
718
  '</div>',
669
719
  '</div>',
@@ -688,6 +738,8 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
688
738
  button.classList.toggle('active', button.getAttribute('data-type') === activeShortcut);
689
739
  });
690
740
 
741
+ syncUrl();
742
+
691
743
  if (state.page === 'overview') await renderOverview(main);
692
744
  if (state.page === 'entries') await renderEntries(main);
693
745
  if (state.page === 'monitoring') await renderMonitoring(main);
@@ -822,7 +874,12 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
822
874
  const action = target.getAttribute('data-action');
823
875
  if (action === 'go-page') { setPage(String(target.getAttribute('data-page') || 'overview')); return; }
824
876
  if (action === 'type-shortcut') { setTypeShortcut(String(target.getAttribute('data-type') || '')); return; }
825
- if (action === 'filter-tag') { filterByTag(String(target.getAttribute('data-tag') || '')); return; }
877
+ if (action === 'filter-tag') {
878
+ if (target instanceof HTMLAnchorElement && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)) return;
879
+ event.preventDefault();
880
+ filterByTag(String(target.getAttribute('data-tag') || ''));
881
+ return;
882
+ }
826
883
  if (action === 'detail-tab') { state = { ...state, detailTab: String(target.getAttribute('data-tab') || 'summary') }; render(); return; }
827
884
  if (action === 'clear-all') { clearAll(); return; }
828
885
  if (action === 'show-detail') { showDetail(String(target.getAttribute('data-uuid') || '')); return; }
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  export { TraceConfig } from './config';
9
9
  export { TraceStorage } from './storage';
10
10
  export type { ITraceStorage } from './storage';
11
+ export { TraceContentRedaction } from './storage/TraceContentRedaction';
11
12
  export { TraceContext } from './context';
12
13
  export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
13
14
  export type { TraceDashboardOptions, TraceDashboardRegistrationOptions } from './dashboard/routes';
@@ -31,5 +32,6 @@ export { QueryWatcher } from './watchers/QueryWatcher';
31
32
  export { RedisWatcher } from './watchers/RedisWatcher';
32
33
  export { ScheduleWatcher } from './watchers/ScheduleWatcher';
33
34
  export { ViewWatcher } from './watchers/ViewWatcher';
35
+ export declare const captureTraceException: (error: unknown) => void;
34
36
  export { EntryType } from './types';
35
37
  export type { AuthContent, BatchContent, CacheContent, ClientRequestContent, CommandContent, DumpContent, EntryTypeValue, EventContent, ExceptionContent, GateContent, ITraceConfig, ITraceEntry, ITraceWatcher, ITraceWatcherConfig, JobContent, LogContent, MailContent, MiddlewareContent, ModelContent, NotificationContent, QueryContent, RedactionConfig, RedisContent, RequestContent, ScheduleContent, TraceConfigOverrides, ViewContent, WatcherToggles, } from './types';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * individually. For full auto-initialisation, use:
6
6
  * import '@zintrust/trace/register';
7
7
  */
8
+ import { ExceptionWatcher as ExceptionWatcherApi } from './watchers/ExceptionWatcher.js';
8
9
  // ---------------------------------------------------------------------------
9
10
  // Config
10
11
  // ---------------------------------------------------------------------------
@@ -13,6 +14,7 @@ export { TraceConfig } from './config.js';
13
14
  // Storage
14
15
  // ---------------------------------------------------------------------------
15
16
  export { TraceStorage } from './storage/index.js';
17
+ export { TraceContentRedaction } from './storage/TraceContentRedaction.js';
16
18
  // ---------------------------------------------------------------------------
17
19
  // Context
18
20
  // ---------------------------------------------------------------------------
@@ -44,6 +46,9 @@ export { QueryWatcher } from './watchers/QueryWatcher.js';
44
46
  export { RedisWatcher } from './watchers/RedisWatcher.js';
45
47
  export { ScheduleWatcher } from './watchers/ScheduleWatcher.js';
46
48
  export { ViewWatcher } from './watchers/ViewWatcher.js';
49
+ export const captureTraceException = (error) => {
50
+ ExceptionWatcherApi.capture(error);
51
+ };
47
52
  // ---------------------------------------------------------------------------
48
53
  // Types
49
54
  // ---------------------------------------------------------------------------
@@ -14,7 +14,7 @@ export const migration = {
14
14
  table.string('type');
15
15
  table.text('content');
16
16
  table.boolean('is_latest').default(true);
17
- table.integer('created_at');
17
+ table.bigInteger('created_at');
18
18
  table.index('batch_id');
19
19
  table.index('family_hash');
20
20
  table.index('created_at');
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Migration: WidenTraceCreatedAtForSql
3
+ * Ensures SQL engines that treat INTEGER as 32-bit can store millisecond timestamps.
4
+ */
5
+ import { type IDatabase } from '@zintrust/core';
6
+ export interface Migration {
7
+ up(db: IDatabase): Promise<void>;
8
+ down(db: IDatabase): Promise<void>;
9
+ }
10
+ export declare const migration: Migration;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Migration: WidenTraceCreatedAtForSql
3
+ * Ensures SQL engines that treat INTEGER as 32-bit can store millisecond timestamps.
4
+ */
5
+ import { MigrationSchema } from '@zintrust/core';
6
+ const alterCreatedAt = async (db) => {
7
+ const driver = db.getType?.() ?? 'sqlite';
8
+ if (driver === 'sqlite' || driver === 'd1' || driver === 'd1-remote')
9
+ return;
10
+ const schema = MigrationSchema.create(db);
11
+ if (!(await schema.hasTable('zin_trace_entries')))
12
+ return;
13
+ if (!(await schema.hasColumn('zin_trace_entries', 'created_at')))
14
+ return;
15
+ if (driver === 'mysql') {
16
+ await db.query('ALTER TABLE zin_trace_entries MODIFY COLUMN created_at BIGINT UNSIGNED NOT NULL', []);
17
+ return;
18
+ }
19
+ if (driver === 'postgresql') {
20
+ await db.query('ALTER TABLE zin_trace_entries ALTER COLUMN created_at TYPE BIGINT USING created_at::bigint', []);
21
+ return;
22
+ }
23
+ if (driver === 'sqlserver') {
24
+ await db.query('ALTER TABLE zin_trace_entries ALTER COLUMN created_at BIGINT NOT NULL', []);
25
+ }
26
+ };
27
+ export const migration = {
28
+ async up(db) {
29
+ await alterCreatedAt(db);
30
+ },
31
+ async down(_db) {
32
+ return;
33
+ },
34
+ };
@@ -1,4 +1,5 @@
1
1
  import { migration as createEntries } from './20260331000001_create_zin_trace_entries_table.js';
2
2
  import { migration as createEntriesTags } from './20260331000002_create_zin_trace_entries_tags_table.js';
3
3
  import { migration as createMonitoring } from './20260331000003_create_zin_trace_monitoring_table.js';
4
- export const migrations = [createEntries, createEntriesTags, createMonitoring];
4
+ import { migration as widenCreatedAt } from './20260407193000_widen_trace_created_at_for_sql.js';
5
+ export const migrations = [createEntries, createEntriesTags, createMonitoring, widenCreatedAt];
package/dist/register.js CHANGED
@@ -22,6 +22,9 @@
22
22
  import { TraceConfig } from './config.js';
23
23
  import { TraceContext } from './context.js';
24
24
  import { TraceStorage } from './storage/index.js';
25
+ import { TraceContentRedaction } from './storage/TraceContentRedaction.js';
26
+ import { TraceEntryFiltering } from './storage/TraceEntryFiltering.js';
27
+ import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics.js';
25
28
  const globalTraceRegisterState = globalThis;
26
29
  globalTraceRegisterState.__zintrust_system_trace_plugin_requested__ = true;
27
30
  const traceAlreadyInitialized = globalTraceRegisterState.__zintrust_system_trace_register_initialized__ === true;
@@ -61,25 +64,120 @@ const resolveTraceConnectionName = (env, configuredConnection) => {
61
64
  }
62
65
  return resolveDefaultConnection();
63
66
  };
67
+ const isObjectValue = (value) => {
68
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
69
+ };
70
+ const parseEnvList = (rawValue) => {
71
+ const value = rawValue.trim();
72
+ if (value === '')
73
+ return undefined;
74
+ if (value.startsWith('[')) {
75
+ try {
76
+ const parsed = JSON.parse(value);
77
+ if (Array.isArray(parsed)) {
78
+ return parsed
79
+ .filter((entry) => typeof entry === 'string')
80
+ .map((entry) => entry.trim())
81
+ .filter((entry) => entry !== '');
82
+ }
83
+ }
84
+ catch {
85
+ // fall through to CSV parsing
86
+ }
87
+ }
88
+ return value
89
+ .split(',')
90
+ .map((entry) => entry.trim())
91
+ .filter((entry) => entry !== '');
92
+ };
93
+ const resolveTraceStartupOverrides = (core) => {
94
+ const traceConfigFile = core.StartupConfigFile?.Trace;
95
+ if (typeof traceConfigFile !== 'string' || traceConfigFile.trim() === '')
96
+ return undefined;
97
+ const overrides = core.StartupConfigFileRegistry?.get(traceConfigFile);
98
+ return isObjectValue(overrides) ? overrides : undefined;
99
+ };
100
+ const buildTraceRedactionOverrides = (input) => {
101
+ const redaction = {
102
+ ...(isObjectValue(input.startupOverrides?.redaction) ? input.startupOverrides?.redaction : {}),
103
+ };
104
+ if (input.redactionKeys === undefined) {
105
+ // no-op
106
+ }
107
+ else {
108
+ redaction.keys = input.redactionKeys;
109
+ }
110
+ if (input.redactionHeaders === undefined) {
111
+ // no-op
112
+ }
113
+ else {
114
+ redaction.headers = input.redactionHeaders;
115
+ }
116
+ if (input.redactionBody === undefined) {
117
+ // no-op
118
+ }
119
+ else {
120
+ redaction.body = input.redactionBody;
121
+ }
122
+ if (input.redactionQuery === undefined) {
123
+ // no-op
124
+ }
125
+ else {
126
+ redaction.query = input.redactionQuery;
127
+ }
128
+ return Object.keys(redaction).length > 0
129
+ ? redaction
130
+ : undefined;
131
+ };
64
132
  const core = (await importCore());
65
133
  const Env = core.Env;
134
+ const startupOverrides = resolveTraceStartupOverrides(core);
66
135
  if (!traceAlreadyInitialized && Env) {
67
- const enabled = Env.getBool('TRACE_ENABLED', false);
136
+ const enabled = startupOverrides?.enabled === true || Env.getBool('TRACE_ENABLED', false);
68
137
  if (enabled) {
69
- const connection = Env.get('TRACE_DB_CONNECTION', '') || undefined;
70
- const pruneAfterHours = Env.getInt('TRACE_PRUNE_HOURS', 24);
71
- const slowQueryThreshold = Env.getInt('TRACE_SLOW_QUERY_MS', 100);
72
- const logMinLevel = Env.get('TRACE_LOG_LEVEL', 'info');
138
+ const connectionRaw = Env.get('TRACE_DB_CONNECTION', '').trim();
139
+ const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
140
+ const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
141
+ const logMinLevelRaw = Env.get('TRACE_LOG_LEVEL', '').trim();
142
+ const redactionKeys = parseEnvList(Env.get('TRACE_REDACT_KEYS', ''));
143
+ const redactionHeaders = parseEnvList(Env.get('TRACE_REDACT_HEADERS', ''));
144
+ const redactionBody = parseEnvList(Env.get('TRACE_REDACT_BODY', ''));
145
+ const redactionQuery = parseEnvList(Env.get('TRACE_REDACT_QUERY', ''));
146
+ const connection = connectionRaw === '' ? startupOverrides?.connection : connectionRaw;
147
+ const pruneAfterHours = pruneAfterHoursRaw === ''
148
+ ? startupOverrides?.pruneAfterHours
149
+ : Number.parseInt(pruneAfterHoursRaw, 10);
150
+ const slowQueryThreshold = slowQueryThresholdRaw === ''
151
+ ? startupOverrides?.slowQueryThreshold
152
+ : Number.parseInt(slowQueryThresholdRaw, 10);
153
+ const logMinLevel = (logMinLevelRaw === '' ? startupOverrides?.logMinLevel : logMinLevelRaw);
154
+ const redaction = buildTraceRedactionOverrides({
155
+ startupOverrides,
156
+ redactionBody,
157
+ redactionHeaders,
158
+ redactionKeys,
159
+ redactionQuery,
160
+ });
73
161
  const config = TraceConfig.merge({
162
+ ...startupOverrides,
74
163
  enabled,
75
164
  connection,
76
- pruneAfterHours,
77
- slowQueryThreshold,
165
+ ...(typeof pruneAfterHours === 'number' && Number.isFinite(pruneAfterHours)
166
+ ? { pruneAfterHours }
167
+ : {}),
168
+ ...(typeof slowQueryThreshold === 'number' && Number.isFinite(slowQueryThreshold)
169
+ ? { slowQueryThreshold }
170
+ : {}),
78
171
  logMinLevel,
172
+ ...(redaction === undefined ? {} : { redaction }),
79
173
  });
80
- const db = core.useDatabase?.(undefined, resolveTraceConnectionName(Env, connection));
174
+ const resolvedConnectionName = resolveTraceConnectionName(Env, config.connection);
175
+ const db = core.useDatabase?.(undefined, resolvedConnectionName);
81
176
  if (db) {
82
- const storage = TraceStorage.resolveStorage(db);
177
+ const storage = TraceWriteDiagnostics.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(db), config), config.redaction), {
178
+ connectionName: resolvedConnectionName,
179
+ logger: core.Logger,
180
+ });
83
181
  if (core.RequestContext) {
84
182
  TraceContext.setRequestContextImpl(core.RequestContext);
85
183
  }
@@ -0,0 +1,4 @@
1
+ import type { ITraceStorage, RedactionConfig } from '../types';
2
+ export declare const TraceContentRedaction: Readonly<{
3
+ wrapStorage(storage: ITraceStorage, redaction: RedactionConfig): ITraceStorage;
4
+ }>;
@@ -0,0 +1,33 @@
1
+ import { redactUnknown } from '../utils/redact.js';
2
+ const collectRedactionFields = (redaction) => {
3
+ return [
4
+ ...new Set([...redaction.keys, ...redaction.headers, ...redaction.body, ...redaction.query]),
5
+ ];
6
+ };
7
+ const redactTraceEntry = (entry, redaction) => {
8
+ return {
9
+ ...entry,
10
+ content: redactUnknown(entry.content, collectRedactionFields(redaction)),
11
+ };
12
+ };
13
+ const redactTracePatch = (patch, redaction) => {
14
+ if (patch.content === undefined)
15
+ return patch;
16
+ return {
17
+ ...patch,
18
+ content: redactUnknown(patch.content, collectRedactionFields(redaction)),
19
+ };
20
+ };
21
+ export const TraceContentRedaction = Object.freeze({
22
+ wrapStorage(storage, redaction) {
23
+ return Object.freeze({
24
+ ...storage,
25
+ writeEntry: async (entry) => {
26
+ await storage.writeEntry(redactTraceEntry(entry, redaction));
27
+ },
28
+ updateEntry: async (uuid, patch) => {
29
+ await storage.updateEntry(uuid, redactTracePatch(patch, redaction));
30
+ },
31
+ });
32
+ },
33
+ });
@@ -0,0 +1,4 @@
1
+ import type { ITraceConfig, ITraceStorage } from '../types';
2
+ export declare const TraceEntryFiltering: Readonly<{
3
+ wrapStorage(storage: ITraceStorage, config: ITraceConfig): ITraceStorage;
4
+ }>;
@@ -0,0 +1,13 @@
1
+ import { TraceEntryFilter } from '../utils/entryFilter.js';
2
+ export const TraceEntryFiltering = Object.freeze({
3
+ wrapStorage(storage, config) {
4
+ return Object.freeze({
5
+ ...storage,
6
+ async writeEntry(entry) {
7
+ if (!TraceEntryFilter.shouldCapture(entry, config))
8
+ return;
9
+ await storage.writeEntry(entry);
10
+ },
11
+ });
12
+ },
13
+ });
@@ -3,6 +3,38 @@ const TABLE_ENTRIES = 'zin_trace_entries';
3
3
  const TABLE_TAGS = 'zin_trace_entries_tags';
4
4
  const TABLE_MONITORING = 'zin_trace_monitoring';
5
5
  const generateUuid = () => crypto.randomUUID();
6
+ const buildIgnoreInsert = (db, table, columns, conflictColumns) => {
7
+ const columnList = columns.join(', ');
8
+ const placeholders = columns.map(() => '?').join(', ');
9
+ const driver = db.getType?.() ?? 'sqlite';
10
+ if (driver === 'sqlite' || driver === 'd1' || driver === 'd1-remote') {
11
+ return `INSERT OR IGNORE INTO ${table} (${columnList}) VALUES (${placeholders})`;
12
+ }
13
+ if (driver === 'mysql') {
14
+ return `INSERT IGNORE INTO ${table} (${columnList}) VALUES (${placeholders})`;
15
+ }
16
+ if (driver === 'postgresql') {
17
+ return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${conflictColumns.join(', ')}) DO NOTHING`;
18
+ }
19
+ if (driver === 'sqlserver') {
20
+ const sourceColumns = columns.map((_, index) => `v${index + 1}`);
21
+ const selectClause = sourceColumns.map((name) => `? AS ${name}`).join(', ');
22
+ const conflictClause = conflictColumns
23
+ .map((column) => `target.${column} = source.${column}`)
24
+ .join(' AND ');
25
+ const insertValues = columns.map((column) => `source.${column}`).join(', ');
26
+ const sourceProjection = columns
27
+ .map((column, index) => `${sourceColumns[index]} AS ${column}`)
28
+ .join(', ');
29
+ return [
30
+ `MERGE INTO ${table} WITH (HOLDLOCK) AS target`,
31
+ `USING (SELECT ${sourceProjection} FROM (SELECT ${selectClause}) seed) AS source`,
32
+ `ON ${conflictClause}`,
33
+ `WHEN NOT MATCHED THEN INSERT (${columnList}) VALUES (${insertValues});`,
34
+ ].join(' ');
35
+ }
36
+ return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders})`;
37
+ };
6
38
  const rowToEntry = (row, tags) => ({
7
39
  uuid: row.uuid,
8
40
  batchId: row.batch_id,
@@ -16,11 +48,9 @@ const rowToEntry = (row, tags) => ({
16
48
  const insertTags = async (db, uuid, tags) => {
17
49
  if (tags.length === 0)
18
50
  return;
51
+ const sql = buildIgnoreInsert(db, TABLE_TAGS, ['entry_uuid', 'tag'], ['entry_uuid', 'tag']);
19
52
  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
- ]);
53
+ await db.execute(sql, [uuid, tag]);
24
54
  }));
25
55
  };
26
56
  const buildEntryFilters = (opts) => {
@@ -158,7 +188,7 @@ const createStorage = (db) => {
158
188
  return rows.map((row) => row.tag);
159
189
  };
160
190
  const addMonitoring = async (tag) => {
161
- await db.execute(`INSERT OR IGNORE INTO ${TABLE_MONITORING} (tag) VALUES (?)`, [tag]);
191
+ await db.execute(buildIgnoreInsert(db, TABLE_MONITORING, ['tag'], ['tag']), [tag]);
162
192
  };
163
193
  const removeMonitoring = async (tag) => {
164
194
  await db.execute(`DELETE FROM ${TABLE_MONITORING} WHERE tag = ?`, [tag]);