@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
@@ -0,0 +1,19 @@
1
+ import type { ITraceStorage } from '../types';
2
+ type TraceLogger = {
3
+ warn: (message: string, context?: Record<string, unknown>) => void;
4
+ };
5
+ type TraceWriteDiagnosticsSnapshot = {
6
+ degraded: boolean;
7
+ lastErrorMessage: string | null;
8
+ lastFailureAt: number | null;
9
+ totalFailures: number;
10
+ };
11
+ export declare const TraceWriteDiagnostics: Readonly<{
12
+ getSnapshot(): TraceWriteDiagnosticsSnapshot;
13
+ reset(): void;
14
+ wrapStorage(storage: ITraceStorage, options: {
15
+ connectionName: string;
16
+ logger?: TraceLogger;
17
+ }): ITraceStorage;
18
+ }>;
19
+ export {};
@@ -0,0 +1,98 @@
1
+ const LOG_WINDOW_MS = 30_000;
2
+ const diagnosticsState = {
3
+ degraded: false,
4
+ lastErrorMessage: null,
5
+ lastFailureAt: null,
6
+ lastLoggedAtByFingerprint: new Map(),
7
+ totalFailures: 0,
8
+ };
9
+ const getErrorMessage = (error) => {
10
+ if (error instanceof Error && error.message.trim() !== '')
11
+ return error.message;
12
+ if (typeof error === 'string' && error.trim() !== '')
13
+ return error;
14
+ try {
15
+ const serialized = JSON.stringify(error);
16
+ if (typeof serialized === 'string' && serialized !== '')
17
+ return serialized;
18
+ }
19
+ catch {
20
+ // ignore serialization failures
21
+ }
22
+ return 'Unknown trace storage error';
23
+ };
24
+ const buildFingerprint = (context) => {
25
+ return [
26
+ context.connectionName,
27
+ context.operation,
28
+ context.watcherType ?? 'unknown',
29
+ getErrorMessage(context.error),
30
+ ].join('|');
31
+ };
32
+ const reportFailure = (logger, context) => {
33
+ const now = Date.now();
34
+ const errorMessage = getErrorMessage(context.error);
35
+ const fingerprint = buildFingerprint(context);
36
+ const lastLoggedAt = diagnosticsState.lastLoggedAtByFingerprint.get(fingerprint);
37
+ diagnosticsState.degraded = true;
38
+ diagnosticsState.lastErrorMessage = errorMessage;
39
+ diagnosticsState.lastFailureAt = now;
40
+ diagnosticsState.totalFailures += 1;
41
+ if (!logger)
42
+ return;
43
+ if (typeof lastLoggedAt === 'number' && now - lastLoggedAt < LOG_WINDOW_MS)
44
+ return;
45
+ diagnosticsState.lastLoggedAtByFingerprint.set(fingerprint, now);
46
+ logger.warn('[trace] Trace storage write degraded', {
47
+ connectionName: context.connectionName,
48
+ error: errorMessage,
49
+ lastFailureAt: now,
50
+ operation: context.operation,
51
+ totalFailures: diagnosticsState.totalFailures,
52
+ watcherType: context.watcherType ?? null,
53
+ });
54
+ };
55
+ const wrapStorageMethod = (method, describeFailure, connectionName, logger) => {
56
+ return async (...args) => {
57
+ try {
58
+ return await method(...args);
59
+ }
60
+ catch (error) {
61
+ reportFailure(logger, {
62
+ ...describeFailure(...args),
63
+ connectionName,
64
+ error,
65
+ });
66
+ throw error;
67
+ }
68
+ };
69
+ };
70
+ export const TraceWriteDiagnostics = Object.freeze({
71
+ getSnapshot() {
72
+ return {
73
+ degraded: diagnosticsState.degraded,
74
+ lastErrorMessage: diagnosticsState.lastErrorMessage,
75
+ lastFailureAt: diagnosticsState.lastFailureAt,
76
+ totalFailures: diagnosticsState.totalFailures,
77
+ };
78
+ },
79
+ reset() {
80
+ diagnosticsState.degraded = false;
81
+ diagnosticsState.lastErrorMessage = null;
82
+ diagnosticsState.lastFailureAt = null;
83
+ diagnosticsState.totalFailures = 0;
84
+ diagnosticsState.lastLoggedAtByFingerprint.clear();
85
+ },
86
+ wrapStorage(storage, options) {
87
+ return Object.freeze({
88
+ ...storage,
89
+ writeEntry: wrapStorageMethod(storage.writeEntry.bind(storage), (entry) => ({ operation: 'writeEntry', watcherType: entry.type }), options.connectionName, options.logger),
90
+ updateEntry: wrapStorageMethod(storage.updateEntry.bind(storage), (_uuid, _patch) => ({ operation: 'updateEntry' }), options.connectionName, options.logger),
91
+ markFamilyStale: wrapStorageMethod(storage.markFamilyStale.bind(storage), (_familyHash, _exceptUuid) => ({ operation: 'markFamilyStale' }), options.connectionName, options.logger),
92
+ prune: wrapStorageMethod(storage.prune.bind(storage), (_olderThanMs, _keepExceptions) => ({ operation: 'prune' }), options.connectionName, options.logger),
93
+ clear: wrapStorageMethod(storage.clear.bind(storage), () => ({ operation: 'clear' }), options.connectionName, options.logger),
94
+ addMonitoring: wrapStorageMethod(storage.addMonitoring.bind(storage), (_tag) => ({ operation: 'addMonitoring' }), options.connectionName, options.logger),
95
+ removeMonitoring: wrapStorageMethod(storage.removeMonitoring.bind(storage), (_tag) => ({ operation: 'removeMonitoring' }), options.connectionName, options.logger),
96
+ });
97
+ },
98
+ });
package/dist/types.d.ts CHANGED
@@ -33,6 +33,7 @@ export interface RequestContent {
33
33
  payload: Record<string, unknown>;
34
34
  responseStatus: number;
35
35
  responseHeaders: Record<string, string>;
36
+ responseBody?: unknown;
36
37
  duration: number;
37
38
  memory: number | null;
38
39
  middleware: string[];
@@ -231,31 +232,47 @@ export interface ITraceWatcher {
231
232
  register(opts: ITraceWatcherConfig): () => void;
232
233
  }
233
234
  export type RedactionConfig = {
235
+ keys: string[];
234
236
  headers: string[];
235
237
  body: string[];
236
238
  query: string[];
237
239
  };
240
+ export type TraceFilterRule = {
241
+ enabled?: boolean;
242
+ include?: string[];
243
+ exclude?: string[];
244
+ };
245
+ export type TraceRequestWatcherConfig = TraceFilterRule & {
246
+ all?: TraceFilterRule;
247
+ get?: TraceFilterRule;
248
+ post?: TraceFilterRule;
249
+ put?: TraceFilterRule;
250
+ patch?: TraceFilterRule;
251
+ delete?: TraceFilterRule;
252
+ };
253
+ export type TraceWatcherToggle = boolean | TraceFilterRule;
254
+ export type TraceRequestWatcherToggle = boolean | TraceRequestWatcherConfig;
238
255
  export type WatcherToggles = {
239
- request?: boolean;
240
- query?: boolean;
241
- exception?: boolean;
242
- log?: boolean;
243
- job?: boolean;
244
- cache?: boolean;
245
- schedule?: boolean;
246
- mail?: boolean;
247
- auth?: boolean;
248
- event?: boolean;
249
- model?: boolean;
250
- notification?: boolean;
251
- redis?: boolean;
252
- gate?: boolean;
253
- middleware?: boolean;
254
- command?: boolean;
255
- batch?: boolean;
256
- dump?: boolean;
257
- view?: boolean;
258
- clientRequest?: boolean;
256
+ request?: TraceRequestWatcherToggle;
257
+ query?: TraceWatcherToggle;
258
+ exception?: TraceWatcherToggle;
259
+ log?: TraceWatcherToggle;
260
+ job?: TraceWatcherToggle;
261
+ cache?: TraceWatcherToggle;
262
+ schedule?: TraceWatcherToggle;
263
+ mail?: TraceWatcherToggle;
264
+ auth?: TraceWatcherToggle;
265
+ event?: TraceWatcherToggle;
266
+ model?: TraceWatcherToggle;
267
+ notification?: TraceWatcherToggle;
268
+ redis?: TraceWatcherToggle;
269
+ gate?: TraceWatcherToggle;
270
+ middleware?: TraceWatcherToggle;
271
+ command?: TraceWatcherToggle;
272
+ batch?: TraceWatcherToggle;
273
+ dump?: TraceWatcherToggle;
274
+ view?: TraceWatcherToggle;
275
+ clientRequest?: TraceWatcherToggle;
259
276
  };
260
277
  export interface ITraceConfig {
261
278
  enabled: boolean;
@@ -0,0 +1,4 @@
1
+ import type { ITraceConfig, ITraceEntry } from '../types';
2
+ export declare const TraceEntryFilter: Readonly<{
3
+ shouldCapture(entry: ITraceEntry, config: ITraceConfig): boolean;
4
+ }>;
@@ -0,0 +1,95 @@
1
+ import { EntryType } from '../types.js';
2
+ const isObjectValue = (value) => {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ };
5
+ const normalizeTerms = (terms) => {
6
+ if (!Array.isArray(terms))
7
+ return [];
8
+ return terms
9
+ .filter((term) => typeof term === 'string')
10
+ .map((term) => term.trim().toLowerCase())
11
+ .filter((term) => term !== '');
12
+ };
13
+ const matchesRule = (haystack, rule) => {
14
+ if (!rule)
15
+ return true;
16
+ const include = normalizeTerms(rule.include);
17
+ const exclude = normalizeTerms(rule.exclude);
18
+ if (exclude.some((term) => haystack.includes(term)))
19
+ return false;
20
+ if (include.length === 0)
21
+ return true;
22
+ return include.some((term) => haystack.includes(term));
23
+ };
24
+ const toSearchableText = (entry) => {
25
+ const sections = [entry.type, entry.batchId, ...(entry.tags ?? [])];
26
+ try {
27
+ sections.push(JSON.stringify(entry.content) ?? '');
28
+ }
29
+ catch {
30
+ sections.push(String(entry.content ?? ''));
31
+ }
32
+ return sections.join(' ').toLowerCase();
33
+ };
34
+ const watcherKeyByEntryType = {
35
+ [EntryType.REQUEST]: 'request',
36
+ [EntryType.QUERY]: 'query',
37
+ [EntryType.EXCEPTION]: 'exception',
38
+ [EntryType.LOG]: 'log',
39
+ [EntryType.JOB]: 'job',
40
+ [EntryType.CACHE]: 'cache',
41
+ [EntryType.SCHEDULE]: 'schedule',
42
+ [EntryType.MAIL]: 'mail',
43
+ [EntryType.AUTH]: 'auth',
44
+ [EntryType.EVENT]: 'event',
45
+ [EntryType.MODEL]: 'model',
46
+ [EntryType.NOTIFICATION]: 'notification',
47
+ [EntryType.REDIS]: 'redis',
48
+ [EntryType.GATE]: 'gate',
49
+ [EntryType.MIDDLEWARE]: 'middleware',
50
+ [EntryType.COMMAND]: 'command',
51
+ [EntryType.BATCH]: 'batch',
52
+ [EntryType.DUMP]: 'dump',
53
+ [EntryType.VIEW]: 'view',
54
+ [EntryType.CLIENT_REQUEST]: 'clientRequest',
55
+ };
56
+ const getRequestMethodRule = (watcher, entry) => {
57
+ if (entry.type !== EntryType.REQUEST)
58
+ return undefined;
59
+ const content = isObjectValue(entry.content) ? entry.content : undefined;
60
+ const methodValue = content?.['method'];
61
+ const method = typeof methodValue === 'string' ? methodValue.trim().toLowerCase() : '';
62
+ if (method === 'get')
63
+ return watcher.get;
64
+ if (method === 'post')
65
+ return watcher.post;
66
+ if (method === 'put')
67
+ return watcher.put;
68
+ if (method === 'patch')
69
+ return watcher.patch;
70
+ if (method === 'delete' || method === 'del')
71
+ return watcher.delete;
72
+ return watcher.all;
73
+ };
74
+ export const TraceEntryFilter = Object.freeze({
75
+ shouldCapture(entry, config) {
76
+ const watcherKey = watcherKeyByEntryType[entry.type];
77
+ const watcher = config.watchers[watcherKey];
78
+ if (watcher === false)
79
+ return false;
80
+ if (!isObjectValue(watcher))
81
+ return true;
82
+ const haystack = toSearchableText(entry);
83
+ if (!matchesRule(haystack, watcher))
84
+ return false;
85
+ if (watcherKey === 'request') {
86
+ const requestWatcher = watcher;
87
+ const methodRule = getRequestMethodRule(requestWatcher, entry);
88
+ if (!matchesRule(haystack, requestWatcher.all))
89
+ return false;
90
+ if (!matchesRule(haystack, methodRule))
91
+ return false;
92
+ }
93
+ return true;
94
+ },
95
+ });
@@ -2,5 +2,6 @@
2
2
  * Redaction helpers for @zintrust/trace watchers.
3
3
  */
4
4
  export declare const redactHeaders: (headers: Record<string, string>, fields: string[]) => Record<string, string>;
5
+ export declare const redactUnknown: (value: unknown, fields: string[]) => unknown;
5
6
  export declare const redactObject: (obj: Record<string, unknown>, fields: string[]) => Record<string, unknown>;
6
7
  export declare const redactString: (value: string, fields: string[]) => string;
@@ -1,7 +1,42 @@
1
1
  /**
2
2
  * Redaction helpers for @zintrust/trace watchers.
3
3
  */
4
- const REDACTED = '[REDACTED]';
4
+ const REDACTED = '****';
5
+ const isArrayValue = (value) => Array.isArray(value);
6
+ const isObjectValue = (value) => {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ };
9
+ const normalizeFields = (fields) => {
10
+ const normalized = new Set();
11
+ for (const field of fields) {
12
+ if (typeof field !== 'string')
13
+ continue;
14
+ const key = field.trim().toLowerCase();
15
+ if (key !== '')
16
+ normalized.add(key);
17
+ }
18
+ return normalized;
19
+ };
20
+ const redactUnknownValue = (value, fields, seen) => {
21
+ if (isArrayValue(value)) {
22
+ return value.map((item) => redactUnknownValue(item, fields, seen));
23
+ }
24
+ if (!isObjectValue(value)) {
25
+ return value;
26
+ }
27
+ if (seen.has(value)) {
28
+ return '[Circular]';
29
+ }
30
+ seen.add(value);
31
+ const out = {};
32
+ for (const [key, entryValue] of Object.entries(value)) {
33
+ out[key] = fields.has(key.toLowerCase())
34
+ ? REDACTED
35
+ : redactUnknownValue(entryValue, fields, seen);
36
+ }
37
+ seen.delete(value);
38
+ return out;
39
+ };
5
40
  const redactQuerySegment = (segment, fields) => {
6
41
  const separatorIndex = segment.indexOf('=');
7
42
  if (separatorIndex <= 0)
@@ -13,23 +48,22 @@ const redactQuerySegment = (segment, fields) => {
13
48
  return `${key}=${REDACTED}`;
14
49
  };
15
50
  export const redactHeaders = (headers, fields) => {
16
- const lower = new Set(fields.map((f) => f.toLowerCase()));
51
+ const lower = normalizeFields(fields);
17
52
  const out = {};
18
53
  for (const [k, v] of Object.entries(headers)) {
19
54
  out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
20
55
  }
21
56
  return out;
22
57
  };
58
+ export const redactUnknown = (value, fields) => {
59
+ return redactUnknownValue(value, normalizeFields(fields), new WeakSet());
60
+ };
23
61
  export const redactObject = (obj, fields) => {
24
- const lower = new Set(fields.map((f) => f.toLowerCase()));
25
- const out = {};
26
- for (const [k, v] of Object.entries(obj)) {
27
- out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
28
- }
29
- return out;
62
+ const redacted = redactUnknown(obj, fields);
63
+ return isObjectValue(redacted) ? redacted : {};
30
64
  };
31
65
  export const redactString = (value, fields) => {
32
- const lower = new Set(fields.map((f) => f.toLowerCase()));
66
+ const lower = normalizeFields(fields);
33
67
  if (value === '')
34
68
  return value;
35
69
  let output = '';
@@ -39,7 +39,7 @@ export const CommandWatcher = Object.freeze({
39
39
  if (config.watchers.command === false)
40
40
  return () => undefined;
41
41
  _storage = storage;
42
- _redactKeys = config.redaction?.body ?? [];
42
+ _redactKeys = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
43
43
  _ignoreRoutes = config.ignoreRoutes;
44
44
  return () => {
45
45
  _storage = null;
@@ -40,7 +40,7 @@ export const HttpClientWatcher = Object.freeze({
40
40
  if (config.watchers.clientRequest === false)
41
41
  return () => undefined;
42
42
  _storage = storage;
43
- _redactHeaderNames = config.redaction?.headers ?? [];
43
+ _redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
44
44
  _ignoreRoutes = config.ignoreRoutes;
45
45
  return () => {
46
46
  _storage = null;
@@ -1,7 +1,7 @@
1
1
  import { TraceContext } from '../context.js';
2
2
  import { EntryType } from '../types.js';
3
3
  import { AuthTag } from '../utils/authTag.js';
4
- import { redactHeaders, redactObject } from '../utils/redact.js';
4
+ import { redactHeaders, redactObject, redactUnknown } from '../utils/redact.js';
5
5
  import { RequestFilter } from '../utils/requestFilter.js';
6
6
  const normalizeHeaders = (headers) => {
7
7
  if (!headers)
@@ -14,16 +14,91 @@ const normalizeHeaders = (headers) => {
14
14
  return [];
15
15
  }));
16
16
  };
17
- const buildEntry = (req, res, start, config) => {
18
- const headers = redactHeaders(normalizeHeaders(req.headers), config.redaction.headers);
19
- const payload = req.body ? redactObject(req.body, config.redaction.body) : {};
17
+ const normalizeHeaderValue = (value) => {
18
+ return Array.isArray(value) ? value.join(', ') : value;
19
+ };
20
+ const registerCompletionHandler = (response, onComplete) => {
21
+ const raw = response.getRaw();
22
+ if (typeof raw.once !== 'function')
23
+ return () => undefined;
24
+ let completed = false;
25
+ const cleanup = () => {
26
+ if (typeof raw.off === 'function') {
27
+ raw.off('finish', markCompleted);
28
+ raw.off('close', markCompleted);
29
+ }
30
+ };
31
+ const markCompleted = () => {
32
+ if (completed)
33
+ return;
34
+ completed = true;
35
+ cleanup();
36
+ onComplete();
37
+ };
38
+ raw.once('finish', markCompleted);
39
+ raw.once('close', markCompleted);
40
+ return cleanup;
41
+ };
42
+ const captureResponse = (response, config) => {
43
+ const headers = {};
44
+ const redactFields = [...config.redaction.keys, ...config.redaction.body];
45
+ const originalSetHeader = response.setHeader;
46
+ const originalJson = response.json;
47
+ const originalText = response.text;
48
+ const originalHtml = response.html;
49
+ const originalSend = response.send;
50
+ const capture = {
51
+ headers,
52
+ body: undefined,
53
+ restore() {
54
+ response.setHeader = originalSetHeader;
55
+ response.json = originalJson;
56
+ response.text = originalText;
57
+ response.html = originalHtml;
58
+ response.send = originalSend;
59
+ },
60
+ };
61
+ response.setHeader = function setHeader(name, value) {
62
+ headers[name] = normalizeHeaderValue(value);
63
+ return originalSetHeader.call(this, name, value);
64
+ };
65
+ response.json = function json(data) {
66
+ capture.body = redactUnknown(data, redactFields);
67
+ originalJson.call(this, data);
68
+ };
69
+ response.text = function text(value) {
70
+ capture.body = value;
71
+ originalText.call(this, value);
72
+ };
73
+ response.html = function html(value) {
74
+ capture.body = value;
75
+ originalHtml.call(this, value);
76
+ };
77
+ response.send = function send(data) {
78
+ capture.body = typeof data === 'string' ? data : `[binary ${data.length} bytes]`;
79
+ originalSend.call(this, data);
80
+ };
81
+ return capture;
82
+ };
83
+ const buildEntry = (req, res, start, config, responseCapture) => {
84
+ const headers = redactHeaders(normalizeHeaders(req.headers), [
85
+ ...config.redaction.keys,
86
+ ...config.redaction.headers,
87
+ ]);
88
+ const payload = req.body
89
+ ? redactObject(req.body, [...config.redaction.keys, ...config.redaction.body])
90
+ : {};
20
91
  return {
21
92
  method: req.getMethod(),
22
93
  uri: req.getPath(),
23
94
  headers,
24
95
  payload,
25
96
  responseStatus: res.getStatus(),
26
- responseHeaders: {},
97
+ responseHeaders: redactHeaders(responseCapture.headers, [
98
+ ...config.redaction.keys,
99
+ ...config.redaction.headers,
100
+ ]),
101
+ responseBody: responseCapture.body,
27
102
  duration: Date.now() - start,
28
103
  memory: TraceContext.getMemory(),
29
104
  middleware: [],
@@ -48,22 +123,31 @@ export const HttpWatcher = Object.freeze({
48
123
  return next();
49
124
  const start = TraceContext.now();
50
125
  const batchId = TraceContext.getBatchId();
126
+ const responseCapture = captureResponse(response, config);
127
+ let didPersist = false;
128
+ const persistEntry = () => {
129
+ if (didPersist)
130
+ return;
131
+ didPersist = true;
132
+ const content = buildEntry(request, response, start, config, responseCapture);
133
+ const tags = AuthTag.append([]);
134
+ if (content.responseStatus >= 500)
135
+ tags.push('failed');
136
+ responseCapture.restore();
137
+ storage
138
+ .writeEntry({
139
+ uuid: crypto.randomUUID(),
140
+ batchId,
141
+ type: EntryType.REQUEST,
142
+ content,
143
+ tags,
144
+ isLatest: true,
145
+ createdAt: TraceContext.now(),
146
+ })
147
+ .catch(() => undefined); // fire-and-forget
148
+ };
149
+ registerCompletionHandler(response, persistEntry);
51
150
  await next();
52
- const content = buildEntry(request, response, start, config);
53
- const tags = AuthTag.append([]);
54
- if (content.responseStatus >= 500)
55
- tags.push('failed');
56
- storage
57
- .writeEntry({
58
- uuid: crypto.randomUUID(),
59
- batchId,
60
- type: EntryType.REQUEST,
61
- content,
62
- tags,
63
- isLatest: true,
64
- createdAt: TraceContext.now(),
65
- })
66
- .catch(() => undefined); // fire-and-forget
67
151
  };
68
152
  registerMiddleware(middleware);
69
153
  return () => undefined;
@@ -6,6 +6,7 @@ import { TraceContext } from '../context.js';
6
6
  import { EntryType } from '../types.js';
7
7
  import { AuthTag } from '../utils/authTag.js';
8
8
  import { RequestFilter } from '../utils/requestFilter.js';
9
+ // type LoggerSink = (level: string, message: string, context?: Record<string, unknown>) => void;
9
10
  const LEVEL_PRIORITY = {
10
11
  debug: 0,
11
12
  info: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.4.76",
3
+ "version": "0.4.77",
4
4
  "description": "Trace assistant for ZinTrust: logs requests, queries, exceptions, jobs, and more.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  "node": ">=20.0.0"
41
41
  },
42
42
  "peerDependencies": {
43
- "@zintrust/core": "^0.4.74"
43
+ "@zintrust/core": "^0.4.77"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
@@ -56,4 +56,4 @@
56
56
  "build": "tsc -p tsconfig.json && tsc -p tsconfig.migrations.json && node ../../scripts/fix-dist-esm-imports.mjs dist",
57
57
  "prepublishOnly": "npm run build"
58
58
  }
59
- }
59
+ }