@zintrust/trace 0.4.96 → 0.5.1
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.
- package/dist/build-manifest.json +16 -16
- package/dist/config.js +1 -1
- package/dist/dashboard/handlers.js +133 -6
- package/dist/dashboard/ui.js +161 -26
- package/dist/storage/TraceStorage.js +157 -3
- package/dist/types.d.ts +17 -0
- package/dist/watchers/HttpClientWatcher.d.ts +1 -1
- package/dist/watchers/HttpClientWatcher.js +29 -9
- package/package.json +3 -3
- package/src/config.ts +1 -1
- package/src/dashboard/handlers.ts +163 -7
- package/src/dashboard/ui.ts +161 -26
- package/src/storage/TraceStorage.ts +194 -4
- package/src/types.ts +19 -0
- package/src/watchers/HttpClientWatcher.ts +49 -17
package/src/dashboard/ui.ts
CHANGED
|
@@ -139,9 +139,6 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
139
139
|
const MOON_ICON = __TRACE_MOON_ICON__;
|
|
140
140
|
const COPY_ICON = __TRACE_COPY_ICON__;
|
|
141
141
|
const DISCLOSURE_ICON = __TRACE_DISCLOSURE_ICON__;
|
|
142
|
-
.panel{border-radius:var(--radius);border:1px solid var(--line);background:var(--surface);box-shadow:var(--shadow);backdrop-filter:blur(16px)}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:16px;margin-bottom:18px}.stat-card{padding:20px;position:relative;overflow:hidden}.stat-card::after{content:'';position:absolute;right:-18px;bottom:-26px;width:92px;height:92px;border-radius:28px;background:linear-gradient(135deg,rgba(56,189,248,.16),rgba(34,197,94,.08));transform:rotate(18deg)}.stat-label{font-size:.74rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800;margin-bottom:12px}.stat-value{font-size:2.25rem;font-weight:800;line-height:1}.stat-meta{margin-top:10px;color:var(--muted);font-size:.9rem}.content-grid{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(320px,.95fr);gap:18px}.side-stack{display:grid;gap:18px}
|
|
143
|
-
.section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:22px 24px 16px}.section-head h3{margin:0;font-size:1.04rem}.section-head p{margin:6px 0 0;color:var(--muted);font-size:.92rem}.toolbar{display:flex;flex-wrap:wrap;gap:10px;padding:0 24px 18px}.control,.toolbar input,.toolbar select{height:44px;border-radius:13px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);padding:0 14px;min-width:0}.toolbar input,.toolbar select{flex:1 1 180px}.toolbar input::placeholder{color:var(--muted)}.btn{height:44px;border:none;border-radius:13px;padding:0 16px;cursor:pointer;font-weight:800}.btn-primary{background:linear-gradient(135deg,var(--accent-strong),var(--accent));color:#fff}.btn-danger{background:rgba(239,68,68,.12);color:var(--danger);border:1px solid rgba(239,68,68,.18)}.btn-ghost{background:var(--surface-soft);color:var(--text);border:1px solid var(--line)}
|
|
144
|
-
.activity-list{list-style:none;margin:0;padding:0 24px 24px}.activity-item{padding:14px 0;border-top:1px solid var(--line)}.activity-item:first-child{border-top:none}.activity-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.activity-time{color:var(--muted);font-size:.85rem}.activity-summary{margin-top:8px;color:var(--text);line-height:1.48}.back-link{display:inline-flex;align-items:center;gap:8px;margin:0 0 14px;color:var(--accent);font-weight:800;cursor:pointer}.detail-card{padding:24px}.detail-meta{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 20px;color:var(--muted);font-size:.9rem;overflow-wrap:anywhere}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.detail-stack{display:grid;gap:16px;margin-top:18px}.detail-box{padding:16px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.detail-box h4{margin:0 0 10px;font-size:.92rem}.detail-box dl{margin:0;display:grid;gap:8px}.detail-box dt{font-size:.76rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.detail-box dd{margin:0;color:var(--text);line-height:1.45;overflow-wrap:anywhere}.trace-tabs{display:flex;gap:10px;flex-wrap:wrap;margin:20px 0 16px}.trace-tab{border:none;border-radius:12px;padding:10px 12px;background:transparent;color:var(--muted);cursor:pointer;box-shadow:inset 0 0 0 1px var(--line);font-weight:800}.trace-tab.active{background:rgba(56,189,248,.12);color:var(--text);box-shadow:inset 0 0 0 1px rgba(56,189,248,.28)}.trace-panel{display:grid;gap:14px}.trace-item{padding:18px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.trace-item-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.trace-item-summary{margin-top:10px;display:grid;gap:10px}.trace-note{color:var(--muted);line-height:1.6}.trace-disclosure{padding:0;overflow:hidden}.trace-disclosure[open]{padding-bottom:18px}.trace-disclosure .trace-item-summary{margin-top:0}.trace-disclosure-body{display:grid;gap:12px;padding:0 18px}.trace-summary{list-style:none;cursor:pointer;padding:18px}.trace-summary::-webkit-details-marker{display:none}.trace-summary-main{display:grid;gap:10px;min-width:0;flex:1}.trace-summary-copy{display:grid;gap:6px;min-width:0}.trace-summary-copy .summary,.trace-summary-copy .summary-sub{display:block;overflow-wrap:anywhere}.trace-disclosure-body .summary-sub{overflow-wrap:anywhere}.trace-summary-icon{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--muted);flex:none;transition:transform .16s ease,color .16s ease}.trace-summary-icon svg{width:14px;height:14px;display:block}.trace-disclosure[open] .trace-summary-icon{transform:rotate(90deg);color:var(--accent)}
|
|
145
142
|
const JSON_HIGHLIGHT_PATTERN = new RegExp(__TRACE_JSON_REGEX__, 'g');
|
|
146
143
|
const SQL_HIGHLIGHT_PATTERN = new RegExp(__TRACE_SQL_REGEX__, 'gim');
|
|
147
144
|
const ENTRY_TYPES = ['request','query','exception','log','job','cache','schedule','mail','auth','event','model','notification','redis','gate','middleware','command','batch','dump','view','client_request'];
|
|
@@ -172,6 +169,28 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
172
169
|
|
|
173
170
|
let state = createInitialState();
|
|
174
171
|
|
|
172
|
+
const DETAIL_BATCH_PAGE_SIZE = 10;
|
|
173
|
+
const DETAIL_BATCH_SCOPE_BY_TAB = Object.freeze({
|
|
174
|
+
queries: { type: 'query' },
|
|
175
|
+
middleware: { type: 'middleware' },
|
|
176
|
+
models: { type: 'model' },
|
|
177
|
+
logs: { type: 'log' },
|
|
178
|
+
exceptions: { type: 'exception' },
|
|
179
|
+
http: { type: 'client_request' },
|
|
180
|
+
cache: { type: 'cache' },
|
|
181
|
+
other: { scope: 'other' }
|
|
182
|
+
});
|
|
183
|
+
const DETAIL_BATCH_COUNT_TYPES = Object.freeze({
|
|
184
|
+
queries: 'query',
|
|
185
|
+
middleware: 'middleware',
|
|
186
|
+
models: 'model',
|
|
187
|
+
logs: 'log',
|
|
188
|
+
exceptions: 'exception',
|
|
189
|
+
http: 'client_request',
|
|
190
|
+
cache: 'cache'
|
|
191
|
+
});
|
|
192
|
+
const DETAIL_BATCH_OTHER_EXCLUDED_TYPES = ['request','query','middleware','model','log','exception','client_request','cache'];
|
|
193
|
+
|
|
175
194
|
let copySequence = 0;
|
|
176
195
|
const copyPayloads = new Map();
|
|
177
196
|
|
|
@@ -328,9 +347,40 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
328
347
|
return raw === '' ? '-' : escapeHtml(raw.slice(0, 8));
|
|
329
348
|
};
|
|
330
349
|
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
350
|
+
const createDetailBatchState = (payload, scope = '', loading = false) => ({
|
|
351
|
+
counts: payload && typeof payload.counts === 'object' && payload.counts !== null ? payload.counts : {},
|
|
352
|
+
entries: payload && Array.isArray(payload.entries) ? payload.entries : [],
|
|
353
|
+
total: payload && Number.isFinite(Number(payload.total)) ? Number(payload.total) : 0,
|
|
354
|
+
page: payload && Number.isFinite(Number(payload.page)) && Number(payload.page) > 0 ? Number(payload.page) : 1,
|
|
355
|
+
perPage: payload && Number.isFinite(Number(payload.perPage)) && Number(payload.perPage) > 0 ? Number(payload.perPage) : DETAIL_BATCH_PAGE_SIZE,
|
|
356
|
+
scope,
|
|
357
|
+
loading
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const detailBatchState = () => {
|
|
361
|
+
const detailBatch = state.detailBatch;
|
|
362
|
+
if (!detailBatch || typeof detailBatch !== 'object' || Array.isArray(detailBatch)) {
|
|
363
|
+
return createDetailBatchState(null);
|
|
364
|
+
}
|
|
365
|
+
return createDetailBatchState(detailBatch, typeof detailBatch.scope === 'string' ? detailBatch.scope : '', detailBatch.loading === true);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const batchEntries = () => detailBatchState().entries;
|
|
369
|
+
const batchCounts = () => detailBatchState().counts;
|
|
370
|
+
const batchCount = (type) => {
|
|
371
|
+
const raw = batchCounts()[type];
|
|
372
|
+
return Number.isFinite(Number(raw)) ? Number(raw) : 0;
|
|
373
|
+
};
|
|
374
|
+
const otherBatchCount = () => Object.entries(batchCounts()).reduce((sum, pair) => {
|
|
375
|
+
return DETAIL_BATCH_OTHER_EXCLUDED_TYPES.includes(pair[0]) ? sum : sum + Number(pair[1] || 0);
|
|
376
|
+
}, 0);
|
|
377
|
+
const resolveDetailBatchQuery = (tab) => DETAIL_BATCH_SCOPE_BY_TAB[tab] || null;
|
|
378
|
+
const resolveDetailBatchCount = (tab) => {
|
|
379
|
+
if (tab === 'other') return otherBatchCount();
|
|
380
|
+
const type = DETAIL_BATCH_COUNT_TYPES[tab];
|
|
381
|
+
return typeof type === 'string' ? batchCount(type) : 0;
|
|
382
|
+
};
|
|
383
|
+
const hasRequestTrace = () => Boolean(state.detail && state.detail.type === 'request');
|
|
334
384
|
|
|
335
385
|
const prettyJson = (value) => {
|
|
336
386
|
try {
|
|
@@ -689,6 +739,27 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
689
739
|
}).join('') + '</div>';
|
|
690
740
|
};
|
|
691
741
|
|
|
742
|
+
const renderDetailBatchPanel = (tab) => {
|
|
743
|
+
const detailBatch = detailBatchState();
|
|
744
|
+
const count = resolveDetailBatchCount(tab);
|
|
745
|
+
if (detailBatch.loading && detailBatch.scope === tab) {
|
|
746
|
+
return '<p class="trace-note">Loading related entries...</p>';
|
|
747
|
+
}
|
|
748
|
+
if (count === 0) {
|
|
749
|
+
return '<p class="trace-note">No related entries captured.</p>';
|
|
750
|
+
}
|
|
751
|
+
if (detailBatch.scope !== tab) {
|
|
752
|
+
return '<p class="trace-note">Open this tab to load the first page of related entries.</p>';
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const totalPages = Math.max(1, Math.ceil(detailBatch.total / Math.max(1, detailBatch.perPage)));
|
|
756
|
+
|
|
757
|
+
return [
|
|
758
|
+
renderTraceItems(batchEntries()),
|
|
759
|
+
'<div class="pagination"><span>Page ' + escapeHtml(detailBatch.page) + ' of ' + escapeHtml(totalPages) + ' · ' + escapeHtml(detailBatch.total) + ' related entries</span><div class="pagination-controls"><button type="button" data-action="detail-batch-prev"' + (detailBatch.page <= 1 ? ' disabled' : '') + '>Previous</button><button type="button" data-action="detail-batch-next"' + (detailBatch.page >= totalPages ? ' disabled' : '') + '>Next</button></div></div>'
|
|
760
|
+
].join('');
|
|
761
|
+
};
|
|
762
|
+
|
|
692
763
|
const renderRequestTrace = (main) => {
|
|
693
764
|
const entry = state.detail;
|
|
694
765
|
const content = entry && entry.content ? entry.content : {};
|
|
@@ -697,17 +768,16 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
697
768
|
{ id: 'payload', label: 'Payload' },
|
|
698
769
|
{ id: 'headers', label: 'Headers' },
|
|
699
770
|
{ id: 'response', label: 'Response' },
|
|
700
|
-
{ id: 'queries', label: 'Queries', count:
|
|
701
|
-
{ id: 'middleware', label: 'Middleware', count:
|
|
702
|
-
{ id: 'models', label: 'Models', count:
|
|
703
|
-
{ id: 'logs', label: 'Logs', count:
|
|
704
|
-
{ id: 'exceptions', label: 'Exceptions', count:
|
|
705
|
-
{ id: 'http', label: 'HTTP', count:
|
|
706
|
-
{ id: 'cache', label: 'Cache', count:
|
|
707
|
-
{ id: 'other', label: 'Other', count:
|
|
771
|
+
{ id: 'queries', label: 'Queries', count: resolveDetailBatchCount('queries') },
|
|
772
|
+
{ id: 'middleware', label: 'Middleware', count: resolveDetailBatchCount('middleware') },
|
|
773
|
+
{ id: 'models', label: 'Models', count: resolveDetailBatchCount('models') },
|
|
774
|
+
{ id: 'logs', label: 'Logs', count: resolveDetailBatchCount('logs') },
|
|
775
|
+
{ id: 'exceptions', label: 'Exceptions', count: resolveDetailBatchCount('exceptions') },
|
|
776
|
+
{ id: 'http', label: 'HTTP', count: resolveDetailBatchCount('http') },
|
|
777
|
+
{ id: 'cache', label: 'Cache', count: resolveDetailBatchCount('cache') },
|
|
778
|
+
{ id: 'other', label: 'Other', count: resolveDetailBatchCount('other') }
|
|
708
779
|
];
|
|
709
780
|
const currentTab = traceTabs.some((tab) => tab.id === state.detailTab) ? state.detailTab : 'summary';
|
|
710
|
-
const otherEntries = batchEntries().filter((item) => !['request','query','middleware','model','log','exception','client_request','cache'].includes(item.type));
|
|
711
781
|
const panels = {
|
|
712
782
|
summary: [
|
|
713
783
|
'<div class="detail-grid">',
|
|
@@ -734,14 +804,14 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
734
804
|
payload: detailJson(content.payload || {}, 'Payload Json'),
|
|
735
805
|
headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
736
806
|
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>',
|
|
737
|
-
queries:
|
|
738
|
-
middleware:
|
|
739
|
-
models:
|
|
740
|
-
logs:
|
|
741
|
-
exceptions:
|
|
742
|
-
http:
|
|
743
|
-
cache:
|
|
744
|
-
other:
|
|
807
|
+
queries: renderDetailBatchPanel('queries'),
|
|
808
|
+
middleware: renderDetailBatchPanel('middleware'),
|
|
809
|
+
models: renderDetailBatchPanel('models'),
|
|
810
|
+
logs: renderDetailBatchPanel('logs'),
|
|
811
|
+
exceptions: renderDetailBatchPanel('exceptions'),
|
|
812
|
+
http: renderDetailBatchPanel('http'),
|
|
813
|
+
cache: renderDetailBatchPanel('cache'),
|
|
814
|
+
other: renderDetailBatchPanel('other')
|
|
745
815
|
};
|
|
746
816
|
|
|
747
817
|
main.innerHTML = [
|
|
@@ -961,8 +1031,8 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
961
1031
|
const entry = detailResult.entry;
|
|
962
1032
|
let detailBatch = null;
|
|
963
1033
|
if (entry.type === 'request' && entry.batchId) {
|
|
964
|
-
const batch = await api('/batch/' + encodeURIComponent(entry.batchId));
|
|
965
|
-
detailBatch = batch
|
|
1034
|
+
const batch = await api('/batch/' + encodeURIComponent(entry.batchId) + '?countsOnly=true');
|
|
1035
|
+
detailBatch = createDetailBatchState(batch);
|
|
966
1036
|
}
|
|
967
1037
|
state = { ...state, detail: entry, detailBatch, detailTab: 'summary', page: 'entries' };
|
|
968
1038
|
render();
|
|
@@ -971,6 +1041,54 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
971
1041
|
}
|
|
972
1042
|
};
|
|
973
1043
|
|
|
1044
|
+
const loadDetailBatchTab = async (tab, page = 1) => {
|
|
1045
|
+
const detail = state.detail;
|
|
1046
|
+
if (!detail || detail.type !== 'request' || !detail.batchId) return;
|
|
1047
|
+
|
|
1048
|
+
const query = resolveDetailBatchQuery(tab);
|
|
1049
|
+
if (!query) {
|
|
1050
|
+
state = { ...state, detailTab: tab };
|
|
1051
|
+
render();
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const previous = detailBatchState();
|
|
1056
|
+
state = {
|
|
1057
|
+
...state,
|
|
1058
|
+
detailTab: tab,
|
|
1059
|
+
detailBatch: {
|
|
1060
|
+
...previous,
|
|
1061
|
+
scope: tab,
|
|
1062
|
+
page,
|
|
1063
|
+
perPage: DETAIL_BATCH_PAGE_SIZE,
|
|
1064
|
+
loading: true,
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
render();
|
|
1068
|
+
|
|
1069
|
+
try {
|
|
1070
|
+
const qs = new URLSearchParams({ page: String(page), perPage: String(DETAIL_BATCH_PAGE_SIZE) });
|
|
1071
|
+
if (query.type) qs.set('type', query.type);
|
|
1072
|
+
if (query.scope) qs.set('scope', query.scope);
|
|
1073
|
+
const batch = await api('/batch/' + encodeURIComponent(detail.batchId) + '?' + qs.toString());
|
|
1074
|
+
state = {
|
|
1075
|
+
...state,
|
|
1076
|
+
detailTab: tab,
|
|
1077
|
+
detailBatch: createDetailBatchState(batch, tab, false),
|
|
1078
|
+
page: 'entries'
|
|
1079
|
+
};
|
|
1080
|
+
render();
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
state = {
|
|
1083
|
+
...state,
|
|
1084
|
+
detailTab: tab,
|
|
1085
|
+
detailBatch: { ...previous, loading: false }
|
|
1086
|
+
};
|
|
1087
|
+
render();
|
|
1088
|
+
window.alert(error.message);
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
|
|
974
1092
|
const addTag = async () => {
|
|
975
1093
|
const input = document.getElementById('new-tag');
|
|
976
1094
|
const value = input && 'value' in input ? String(input.value || '').trim() : '';
|
|
@@ -1048,10 +1166,27 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
1048
1166
|
filterByTag(String(target.getAttribute('data-tag') || ''));
|
|
1049
1167
|
return;
|
|
1050
1168
|
}
|
|
1051
|
-
if (action === 'detail-tab') {
|
|
1169
|
+
if (action === 'detail-tab') {
|
|
1170
|
+
const tab = String(target.getAttribute('data-tab') || 'summary');
|
|
1171
|
+
if (Object.prototype.hasOwnProperty.call(DETAIL_BATCH_SCOPE_BY_TAB, tab)) {
|
|
1172
|
+
loadDetailBatchTab(tab, 1);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
state = { ...state, detailTab: tab };
|
|
1176
|
+
render();
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1052
1179
|
if (action === 'clear-all') { clearAll(); return; }
|
|
1053
1180
|
if (action === 'show-detail') { showDetail(String(target.getAttribute('data-uuid') || '')); return; }
|
|
1054
1181
|
if (action === 'close-detail') { state = { ...state, detail: null, detailBatch: null, detailTab: 'summary' }; render(); return; }
|
|
1182
|
+
if (action === 'detail-batch-prev' || action === 'detail-batch-next') {
|
|
1183
|
+
const detailBatch = detailBatchState();
|
|
1184
|
+
const nextPage = action === 'detail-batch-prev' ? Math.max(1, detailBatch.page - 1) : detailBatch.page + 1;
|
|
1185
|
+
if (detailBatch.scope !== '') {
|
|
1186
|
+
loadDetailBatchTab(detailBatch.scope, nextPage);
|
|
1187
|
+
}
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1055
1190
|
if (action === 'page-prev') { state = { ...state, entriesPage: Math.max(1, state.entriesPage - 1) }; render(); return; }
|
|
1056
1191
|
if (action === 'page-next') { state = { ...state, entriesPage: state.entriesPage + 1 }; render(); return; }
|
|
1057
1192
|
if (action === 'clear-filters') { state = { ...state, detail: null, detailBatch: null, detailTab: 'summary', entriesPage: 1, entriesFilter: { type: '', tag: '', batchId: '' } }; render(); return; }
|
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
* read/write operations to the trace storage facade.
|
|
5
5
|
*/
|
|
6
6
|
import type { IDatabase } from '@zintrust/core';
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
EntryTypeValue,
|
|
9
|
+
ITraceEntry,
|
|
10
|
+
ITraceStorage,
|
|
11
|
+
QueryBatchEntriesOptions,
|
|
12
|
+
QueryBatchEntriesResult,
|
|
13
|
+
QueryEntriesOptions,
|
|
14
|
+
} from '../types';
|
|
8
15
|
import { familyHash } from '../utils/familyHash';
|
|
9
16
|
|
|
10
17
|
const TABLE_ENTRIES = 'zin_trace_entries';
|
|
@@ -26,6 +33,102 @@ type EntryRow = {
|
|
|
26
33
|
|
|
27
34
|
type TagRow = { entry_uuid: string; tag: string };
|
|
28
35
|
|
|
36
|
+
const decodeJsonStringLiteral = (value: string): string | undefined => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(`"${value}"`) as string;
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const matchJsonStringField = (content: string, key: string): string | undefined => {
|
|
45
|
+
const match = new RegExp(String.raw`"${key}"\s*:\s*"((?:\\.|[^"\\])*)"`, 's').exec(content);
|
|
46
|
+
return match ? decodeJsonStringLiteral(match[1]) : undefined;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const matchJsonNumberField = (content: string, key: string): number | undefined => {
|
|
50
|
+
const match = new RegExp(String.raw`"${key}"\s*:\s*(-?\d+(?:\.\d+)?)`, 's').exec(content);
|
|
51
|
+
if (!match) return undefined;
|
|
52
|
+
const parsed = Number(match[1]);
|
|
53
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const matchJsonNullOrNumberField = (content: string, key: string): number | null | undefined => {
|
|
57
|
+
const nullMatch = new RegExp(String.raw`"${key}"\s*:\s*null`, 's').exec(content);
|
|
58
|
+
if (nullMatch) return null;
|
|
59
|
+
return matchJsonNumberField(content, key);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const matchJsonStringArrayField = (content: string, key: string): string[] | undefined => {
|
|
63
|
+
const match = new RegExp(String.raw`"${key}"\s*:\s*(\[.*?\])`, 's').exec(content);
|
|
64
|
+
if (!match) return undefined;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(match[1]) as unknown;
|
|
68
|
+
return Array.isArray(parsed)
|
|
69
|
+
? parsed.filter((value): value is string => typeof value === 'string')
|
|
70
|
+
: undefined;
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const compactRequestContent = (content: string): Record<string, unknown> | undefined => {
|
|
77
|
+
const compact: Record<string, unknown> = {};
|
|
78
|
+
const method = matchJsonStringField(content, 'method');
|
|
79
|
+
const uri = matchJsonStringField(content, 'uri');
|
|
80
|
+
const responseStatus = matchJsonNumberField(content, 'responseStatus');
|
|
81
|
+
const duration = matchJsonNumberField(content, 'duration');
|
|
82
|
+
const memory = matchJsonNullOrNumberField(content, 'memory');
|
|
83
|
+
const middleware = matchJsonStringArrayField(content, 'middleware');
|
|
84
|
+
const hostname = matchJsonStringField(content, 'hostname');
|
|
85
|
+
const userId = matchJsonStringField(content, 'userId');
|
|
86
|
+
|
|
87
|
+
if (method !== undefined) compact['method'] = method;
|
|
88
|
+
if (uri !== undefined) compact['uri'] = uri;
|
|
89
|
+
if (responseStatus !== undefined) compact['responseStatus'] = responseStatus;
|
|
90
|
+
if (duration !== undefined) compact['duration'] = duration;
|
|
91
|
+
if (memory !== undefined) compact['memory'] = memory;
|
|
92
|
+
if (middleware !== undefined) compact['middleware'] = middleware;
|
|
93
|
+
if (hostname !== undefined) compact['hostname'] = hostname;
|
|
94
|
+
if (userId !== undefined) compact['userId'] = userId;
|
|
95
|
+
|
|
96
|
+
return Object.keys(compact).length > 0 ? compact : undefined;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const compactClientRequestContent = (content: string): Record<string, unknown> | undefined => {
|
|
100
|
+
const compact: Record<string, unknown> = {};
|
|
101
|
+
const source = matchJsonStringField(content, 'source');
|
|
102
|
+
const method = matchJsonStringField(content, 'method');
|
|
103
|
+
const url = matchJsonStringField(content, 'url');
|
|
104
|
+
const responseStatus = matchJsonNumberField(content, 'responseStatus');
|
|
105
|
+
const error = matchJsonStringField(content, 'error');
|
|
106
|
+
const duration = matchJsonNumberField(content, 'duration');
|
|
107
|
+
const hostname = matchJsonStringField(content, 'hostname');
|
|
108
|
+
|
|
109
|
+
if (source !== undefined) compact['source'] = source;
|
|
110
|
+
if (method !== undefined) compact['method'] = method;
|
|
111
|
+
if (url !== undefined) compact['url'] = url;
|
|
112
|
+
if (responseStatus !== undefined) compact['responseStatus'] = responseStatus;
|
|
113
|
+
if (error !== undefined) compact['error'] = error;
|
|
114
|
+
if (duration !== undefined) compact['duration'] = duration;
|
|
115
|
+
if (hostname !== undefined) compact['hostname'] = hostname;
|
|
116
|
+
|
|
117
|
+
return Object.keys(compact).length > 0 ? compact : undefined;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const summarizeEntryContent = (row: EntryRow): unknown => {
|
|
121
|
+
if (row.type === 'request') {
|
|
122
|
+
return compactRequestContent(row.content) ?? (JSON.parse(row.content) as unknown);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (row.type === 'client_request') {
|
|
126
|
+
return compactClientRequestContent(row.content) ?? (JSON.parse(row.content) as unknown);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return JSON.parse(row.content) as unknown;
|
|
130
|
+
};
|
|
131
|
+
|
|
29
132
|
type DatabaseWithDriver = IDatabase & {
|
|
30
133
|
getType?: () => string;
|
|
31
134
|
};
|
|
@@ -74,12 +177,12 @@ const buildIgnoreInsert = (
|
|
|
74
177
|
return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders})`;
|
|
75
178
|
};
|
|
76
179
|
|
|
77
|
-
const rowToEntry = (row: EntryRow, tags: string[]): ITraceEntry => ({
|
|
180
|
+
const rowToEntry = (row: EntryRow, tags: string[], summary = false): ITraceEntry => ({
|
|
78
181
|
uuid: row.uuid,
|
|
79
182
|
batchId: row.batch_id,
|
|
80
183
|
familyHash: row.family_hash ?? undefined,
|
|
81
184
|
type: row.type as EntryTypeValue,
|
|
82
|
-
content: JSON.parse(row.content) as unknown,
|
|
185
|
+
content: summary ? summarizeEntryContent(row) : (JSON.parse(row.content) as unknown),
|
|
83
186
|
tags,
|
|
84
187
|
isLatest: Boolean(row.is_latest),
|
|
85
188
|
createdAt: row.created_at,
|
|
@@ -132,6 +235,45 @@ const buildEntryFilters = (
|
|
|
132
235
|
return { joinClause, whereClause, params, countParams };
|
|
133
236
|
};
|
|
134
237
|
|
|
238
|
+
const buildBatchCounts = async (
|
|
239
|
+
db: IDatabase,
|
|
240
|
+
batchId: string
|
|
241
|
+
): Promise<Partial<Record<EntryTypeValue, number>>> => {
|
|
242
|
+
const rows = (await db.query(
|
|
243
|
+
`SELECT type, COUNT(*) as cnt FROM ${TABLE_ENTRIES} WHERE batch_id = ? GROUP BY type`,
|
|
244
|
+
[batchId]
|
|
245
|
+
)) as Array<{ type: string; cnt: number }>;
|
|
246
|
+
|
|
247
|
+
const counts: Partial<Record<EntryTypeValue, number>> = {};
|
|
248
|
+
for (const row of rows) {
|
|
249
|
+
counts[row.type as EntryTypeValue] = row.cnt;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return counts;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const buildBatchEntryFilters = (
|
|
256
|
+
batchId: string,
|
|
257
|
+
opts: QueryBatchEntriesOptions
|
|
258
|
+
): { whereClause: string; params: unknown[] } => {
|
|
259
|
+
const conditions = ['batch_id = ?'];
|
|
260
|
+
const params: unknown[] = [batchId];
|
|
261
|
+
|
|
262
|
+
if (opts.type) {
|
|
263
|
+
conditions.push('type = ?');
|
|
264
|
+
params.push(opts.type);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const excludeTypes = opts.excludeTypes ?? [];
|
|
268
|
+
if (excludeTypes.length > 0) {
|
|
269
|
+
const placeholders = excludeTypes.map(() => '?').join(', ');
|
|
270
|
+
conditions.push(`type NOT IN (${placeholders})`);
|
|
271
|
+
params.push(...excludeTypes);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { whereClause: `WHERE ${conditions.join(' AND ')}`, params };
|
|
275
|
+
};
|
|
276
|
+
|
|
135
277
|
const loadTagsByUuid = async (db: IDatabase, uuids: string[]): Promise<Map<string, string[]>> => {
|
|
136
278
|
const tagsByUuid = new Map<string, string[]>();
|
|
137
279
|
if (uuids.length === 0) return tagsByUuid;
|
|
@@ -230,7 +372,7 @@ const createStorage = (db: IDatabase): ITraceStorage => {
|
|
|
230
372
|
);
|
|
231
373
|
|
|
232
374
|
return {
|
|
233
|
-
data: rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? [])),
|
|
375
|
+
data: rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? [], opts.summary)),
|
|
234
376
|
total,
|
|
235
377
|
};
|
|
236
378
|
};
|
|
@@ -273,6 +415,53 @@ const createStorage = (db: IDatabase): ITraceStorage => {
|
|
|
273
415
|
return rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? []));
|
|
274
416
|
};
|
|
275
417
|
|
|
418
|
+
const queryBatchEntries = async (
|
|
419
|
+
batchId: string,
|
|
420
|
+
opts: QueryBatchEntriesOptions = {}
|
|
421
|
+
): Promise<QueryBatchEntriesResult> => {
|
|
422
|
+
const page = opts.page ?? 1;
|
|
423
|
+
const perPage = opts.perPage ?? 10;
|
|
424
|
+
const offset = (page - 1) * perPage;
|
|
425
|
+
const counts = await buildBatchCounts(db, batchId);
|
|
426
|
+
|
|
427
|
+
if (opts.countsOnly) {
|
|
428
|
+
const total = Object.values(counts).reduce((sum, value) => sum + Number(value ?? 0), 0);
|
|
429
|
+
return { entries: [], total, counts, page, perPage };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const { whereClause, params } = buildBatchEntryFilters(batchId, opts);
|
|
433
|
+
const countResult = (await db.queryOne(
|
|
434
|
+
`SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES} ${whereClause}`,
|
|
435
|
+
params
|
|
436
|
+
)) as { cnt: number } | undefined;
|
|
437
|
+
const total = countResult?.cnt ?? 0;
|
|
438
|
+
if (total === 0) {
|
|
439
|
+
return { entries: [], total: 0, counts, page, perPage };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const rows = (await db.query(
|
|
443
|
+
`SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
|
|
444
|
+
FROM ${TABLE_ENTRIES}
|
|
445
|
+
${whereClause}
|
|
446
|
+
ORDER BY created_at ASC, id ASC
|
|
447
|
+
LIMIT ? OFFSET ?`,
|
|
448
|
+
[...params, perPage, offset]
|
|
449
|
+
)) as EntryRow[];
|
|
450
|
+
|
|
451
|
+
const tagsByUuid = await loadTagsByUuid(
|
|
452
|
+
db,
|
|
453
|
+
rows.map((row) => row.uuid)
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
entries: rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? [], opts.summary)),
|
|
458
|
+
total,
|
|
459
|
+
counts,
|
|
460
|
+
page,
|
|
461
|
+
perPage,
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
|
|
276
465
|
const prune = async (olderThanMs: number, keepExceptions = false): Promise<number> => {
|
|
277
466
|
const countResult = (await db.queryOne(
|
|
278
467
|
`SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES}
|
|
@@ -331,6 +520,7 @@ const createStorage = (db: IDatabase): ITraceStorage => {
|
|
|
331
520
|
queryEntries,
|
|
332
521
|
getEntry,
|
|
333
522
|
getBatch,
|
|
523
|
+
queryBatchEntries,
|
|
334
524
|
prune,
|
|
335
525
|
clear,
|
|
336
526
|
getMonitoring,
|
package/src/types.ts
CHANGED
|
@@ -262,6 +262,24 @@ export interface QueryEntriesOptions {
|
|
|
262
262
|
to?: number;
|
|
263
263
|
page?: number;
|
|
264
264
|
perPage?: number;
|
|
265
|
+
summary?: boolean;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface QueryBatchEntriesOptions {
|
|
269
|
+
type?: EntryTypeValue;
|
|
270
|
+
excludeTypes?: EntryTypeValue[];
|
|
271
|
+
page?: number;
|
|
272
|
+
perPage?: number;
|
|
273
|
+
summary?: boolean;
|
|
274
|
+
countsOnly?: boolean;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface QueryBatchEntriesResult {
|
|
278
|
+
entries: ITraceEntry[];
|
|
279
|
+
total: number;
|
|
280
|
+
counts: Partial<Record<EntryTypeValue, number>>;
|
|
281
|
+
page: number;
|
|
282
|
+
perPage: number;
|
|
265
283
|
}
|
|
266
284
|
|
|
267
285
|
export interface ITraceStorage {
|
|
@@ -274,6 +292,7 @@ export interface ITraceStorage {
|
|
|
274
292
|
queryEntries(opts: QueryEntriesOptions): Promise<{ data: ITraceEntry[]; total: number }>;
|
|
275
293
|
getEntry(uuid: string): Promise<ITraceEntry | null>;
|
|
276
294
|
getBatch(batchId: string): Promise<ITraceEntry[]>;
|
|
295
|
+
queryBatchEntries(batchId: string, opts?: QueryBatchEntriesOptions): Promise<QueryBatchEntriesResult>;
|
|
277
296
|
prune(olderThanMs: number, keepExceptions?: boolean): Promise<number>;
|
|
278
297
|
clear(): Promise<void>;
|
|
279
298
|
getMonitoring(): Promise<string[]>;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { TraceContext } from '../context';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import {
|
|
3
|
+
EntryType,
|
|
4
|
+
type ClientRequestContent,
|
|
5
|
+
type ClientRequestTraceInput,
|
|
6
|
+
type ITraceWatcher,
|
|
7
|
+
type ITraceWatcherConfig,
|
|
8
|
+
type TraceClientRequestCaptureRule,
|
|
9
|
+
type TraceClientRequestWatcherConfig,
|
|
9
10
|
} from '../types';
|
|
10
|
-
import { EntryType } from '../types';
|
|
11
11
|
import { AuthTag } from '../utils/authTag';
|
|
12
12
|
import { redactHeaders, redactUnknown } from '../utils/redact';
|
|
13
13
|
import { RequestFilter } from '../utils/requestFilter';
|
|
@@ -85,24 +85,56 @@ const buildResponseBody = (
|
|
|
85
85
|
return { responseBody: redactUnknown(responseBody, _redactBodyFields) };
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
+
const applySource = (content: ClientRequestContent, normalizedSource: string | undefined): void => {
|
|
89
|
+
if (normalizedSource !== undefined) {
|
|
90
|
+
content.source = normalizedSource;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const applyResponseStatus = (
|
|
95
|
+
content: ClientRequestContent,
|
|
96
|
+
responseStatus: number | undefined
|
|
97
|
+
): void => {
|
|
98
|
+
if (responseStatus !== undefined) {
|
|
99
|
+
content.responseStatus = responseStatus;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const applyError = (content: ClientRequestContent, error: unknown): void => {
|
|
104
|
+
if (typeof error === 'string' && error !== '') {
|
|
105
|
+
content.error = error;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const mergePartialContent = (
|
|
110
|
+
content: ClientRequestContent,
|
|
111
|
+
partial: Partial<ClientRequestContent>
|
|
112
|
+
): void => {
|
|
113
|
+
Object.assign(content, partial);
|
|
114
|
+
};
|
|
115
|
+
|
|
88
116
|
const buildClientRequestContent = (
|
|
89
117
|
input: ClientRequestTraceInput,
|
|
90
118
|
sourceRule: TraceClientRequestCaptureRule | undefined,
|
|
91
119
|
normalizedSource: string | undefined
|
|
92
120
|
): ClientRequestContent => {
|
|
93
|
-
|
|
94
|
-
...(normalizedSource === undefined ? {} : { source: normalizedSource }),
|
|
121
|
+
const content: ClientRequestContent = {
|
|
95
122
|
method: input.method.toUpperCase(),
|
|
96
123
|
url: input.url,
|
|
97
|
-
|
|
98
|
-
...buildRequestBody(input.requestBody, sourceRule),
|
|
99
|
-
...(input.responseStatus === undefined ? {} : { responseStatus: input.responseStatus }),
|
|
100
|
-
...buildResponseHeaders(input.responseHeaders, sourceRule),
|
|
101
|
-
...buildResponseBody(input.responseBody, sourceRule),
|
|
102
|
-
...(typeof input.error === 'string' && input.error !== '' ? { error: input.error } : {}),
|
|
124
|
+
requestHeaders: {},
|
|
103
125
|
duration: input.duration,
|
|
104
126
|
hostname: TraceContext.getHostname(),
|
|
105
127
|
};
|
|
128
|
+
|
|
129
|
+
applySource(content, normalizedSource);
|
|
130
|
+
mergePartialContent(content, buildRequestHeaders(input.requestHeaders, sourceRule));
|
|
131
|
+
mergePartialContent(content, buildRequestBody(input.requestBody, sourceRule));
|
|
132
|
+
applyResponseStatus(content, input.responseStatus);
|
|
133
|
+
mergePartialContent(content, buildResponseHeaders(input.responseHeaders, sourceRule));
|
|
134
|
+
mergePartialContent(content, buildResponseBody(input.responseBody, sourceRule));
|
|
135
|
+
applyError(content, input.error);
|
|
136
|
+
|
|
137
|
+
return content;
|
|
106
138
|
};
|
|
107
139
|
|
|
108
140
|
const isWatcherEnabled = (
|
|
@@ -169,7 +201,7 @@ export const HttpClientWatcher: ITraceWatcher & { emit: typeof emit } = Object.f
|
|
|
169
201
|
_storage = storage;
|
|
170
202
|
_clientRequestWatcher =
|
|
171
203
|
typeof config.watchers.clientRequest === 'object' && config.watchers.clientRequest !== null
|
|
172
|
-
?
|
|
204
|
+
? config.watchers.clientRequest
|
|
173
205
|
: undefined;
|
|
174
206
|
_redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
|
|
175
207
|
_redactBodyFields = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
|