@zintrust/trace 0.4.75 → 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 (73) hide show
  1. package/README.md +101 -15
  2. package/dist/build-manifest.json +210 -162
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.js +123 -4
  5. package/dist/dashboard/routes.js +4 -4
  6. package/dist/dashboard/ui.js +80 -23
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +30 -25
  9. package/dist/migrations/{20260331000001_create_zin_debugger_entries_table.d.ts → 20260331000001_create_zin_trace_entries_table.d.ts} +2 -2
  10. package/dist/migrations/{20260331000001_create_zin_debugger_entries_table.js → 20260331000001_create_zin_trace_entries_table.js} +5 -5
  11. package/dist/migrations/{20260331000002_create_zin_debugger_entries_tags_table.d.ts → 20260331000002_create_zin_trace_entries_tags_table.d.ts} +2 -2
  12. package/dist/migrations/{20260331000002_create_zin_debugger_entries_tags_table.js → 20260331000002_create_zin_trace_entries_tags_table.js} +5 -5
  13. package/dist/migrations/{20260331000003_create_zin_debugger_monitoring_table.d.ts → 20260331000003_create_zin_trace_monitoring_table.d.ts} +2 -2
  14. package/dist/migrations/{20260331000003_create_zin_debugger_monitoring_table.js → 20260331000003_create_zin_trace_monitoring_table.js} +4 -4
  15. package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.d.ts +10 -0
  16. package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.js +34 -0
  17. package/dist/migrations/index.d.ts +3 -3
  18. package/dist/migrations/index.js +5 -4
  19. package/dist/register.js +130 -32
  20. package/dist/storage/DebuggerStorage.js +1 -1
  21. package/dist/storage/TraceContentRedaction.d.ts +4 -0
  22. package/dist/storage/TraceContentRedaction.js +33 -0
  23. package/dist/storage/TraceEntryFiltering.d.ts +4 -0
  24. package/dist/storage/TraceEntryFiltering.js +13 -0
  25. package/dist/storage/TraceStorage.js +36 -6
  26. package/dist/storage/TraceWriteDiagnostics.d.ts +19 -0
  27. package/dist/storage/TraceWriteDiagnostics.js +98 -0
  28. package/dist/storage/index.js +1 -1
  29. package/dist/types.d.ts +37 -20
  30. package/dist/ui.js +1 -1
  31. package/dist/utils/authTag.js +1 -1
  32. package/dist/utils/entryFilter.d.ts +4 -0
  33. package/dist/utils/entryFilter.js +95 -0
  34. package/dist/utils/redact.d.ts +1 -0
  35. package/dist/utils/redact.js +43 -9
  36. package/dist/utils/requestFilter.js +1 -1
  37. package/dist/watchers/AuthWatcher.js +3 -3
  38. package/dist/watchers/BatchWatcher.js +3 -3
  39. package/dist/watchers/CacheWatcher.js +5 -5
  40. package/dist/watchers/CommandWatcher.js +5 -5
  41. package/dist/watchers/DumpWatcher.js +3 -3
  42. package/dist/watchers/EventWatcher.js +4 -4
  43. package/dist/watchers/ExceptionWatcher.js +6 -6
  44. package/dist/watchers/GateWatcher.js +3 -3
  45. package/dist/watchers/HttpClientWatcher.js +6 -6
  46. package/dist/watchers/HttpWatcher.js +108 -24
  47. package/dist/watchers/JobWatcher.js +4 -4
  48. package/dist/watchers/LogWatcher.js +5 -4
  49. package/dist/watchers/MailWatcher.js +3 -3
  50. package/dist/watchers/MiddlewareWatcher.js +3 -3
  51. package/dist/watchers/ModelWatcher.js +3 -3
  52. package/dist/watchers/NotificationWatcher.js +4 -4
  53. package/dist/watchers/QueryWatcher.js +5 -5
  54. package/dist/watchers/RedisWatcher.js +4 -4
  55. package/dist/watchers/ScheduleWatcher.js +3 -3
  56. package/dist/watchers/ViewWatcher.js +3 -3
  57. package/package.json +4 -4
  58. package/src/config.ts +152 -5
  59. package/src/dashboard/routes.ts +6 -2
  60. package/src/dashboard/ui.ts +80 -23
  61. package/src/index.ts +7 -0
  62. package/src/register.ts +137 -10
  63. package/src/storage/TraceContentRedaction.ts +44 -0
  64. package/src/storage/TraceEntryFiltering.ts +14 -0
  65. package/src/storage/TraceStorage.ts +52 -5
  66. package/src/storage/TraceWriteDiagnostics.ts +174 -0
  67. package/src/types.ts +40 -20
  68. package/src/utils/entryFilter.ts +108 -0
  69. package/src/utils/redact.ts +57 -9
  70. package/src/watchers/CommandWatcher.ts +1 -1
  71. package/src/watchers/HttpClientWatcher.ts +1 -1
  72. package/src/watchers/HttpWatcher.ts +132 -21
  73. package/src/watchers/LogWatcher.ts +27 -27
@@ -2,7 +2,55 @@
2
2
  * Redaction helpers for @zintrust/trace watchers.
3
3
  */
4
4
 
5
- const REDACTED = '[REDACTED]';
5
+ const REDACTED = '****';
6
+
7
+ const isArrayValue = (value: unknown): value is unknown[] => Array.isArray(value);
8
+
9
+ const isObjectValue = (value: unknown): value is Record<string, unknown> => {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ };
12
+
13
+ const normalizeFields = (fields: string[]): Set<string> => {
14
+ const normalized = new Set<string>();
15
+
16
+ for (const field of fields) {
17
+ if (typeof field !== 'string') continue;
18
+ const key = field.trim().toLowerCase();
19
+ if (key !== '') normalized.add(key);
20
+ }
21
+
22
+ return normalized;
23
+ };
24
+
25
+ const redactUnknownValue = (
26
+ value: unknown,
27
+ fields: Set<string>,
28
+ seen: WeakSet<object>
29
+ ): unknown => {
30
+ if (isArrayValue(value)) {
31
+ return value.map((item) => redactUnknownValue(item, fields, seen));
32
+ }
33
+
34
+ if (!isObjectValue(value)) {
35
+ return value;
36
+ }
37
+
38
+ if (seen.has(value)) {
39
+ return '[Circular]';
40
+ }
41
+
42
+ seen.add(value);
43
+ const out: Record<string, unknown> = {};
44
+
45
+ for (const [key, entryValue] of Object.entries(value)) {
46
+ out[key] = fields.has(key.toLowerCase())
47
+ ? REDACTED
48
+ : redactUnknownValue(entryValue, fields, seen);
49
+ }
50
+
51
+ seen.delete(value);
52
+ return out;
53
+ };
6
54
 
7
55
  const redactQuerySegment = (segment: string, fields: Set<string>): string => {
8
56
  const separatorIndex = segment.indexOf('=');
@@ -19,7 +67,7 @@ export const redactHeaders = (
19
67
  headers: Record<string, string>,
20
68
  fields: string[]
21
69
  ): Record<string, string> => {
22
- const lower = new Set(fields.map((f) => f.toLowerCase()));
70
+ const lower = normalizeFields(fields);
23
71
  const out: Record<string, string> = {};
24
72
  for (const [k, v] of Object.entries(headers)) {
25
73
  out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
@@ -27,20 +75,20 @@ export const redactHeaders = (
27
75
  return out;
28
76
  };
29
77
 
78
+ export const redactUnknown = (value: unknown, fields: string[]): unknown => {
79
+ return redactUnknownValue(value, normalizeFields(fields), new WeakSet<object>());
80
+ };
81
+
30
82
  export const redactObject = (
31
83
  obj: Record<string, unknown>,
32
84
  fields: string[]
33
85
  ): Record<string, unknown> => {
34
- const lower = new Set(fields.map((f) => f.toLowerCase()));
35
- const out: Record<string, unknown> = {};
36
- for (const [k, v] of Object.entries(obj)) {
37
- out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
38
- }
39
- return out;
86
+ const redacted = redactUnknown(obj, fields);
87
+ return isObjectValue(redacted) ? redacted : {};
40
88
  };
41
89
 
42
90
  export const redactString = (value: string, fields: string[]): string => {
43
- const lower = new Set(fields.map((f) => f.toLowerCase()));
91
+ const lower = normalizeFields(fields);
44
92
  if (value === '') return value;
45
93
 
46
94
  let output = '';
@@ -45,7 +45,7 @@ export const CommandWatcher: ITraceWatcher & { emit: typeof emit } = Object.free
45
45
  register({ storage, config }: ITraceWatcherConfig): () => void {
46
46
  if (config.watchers.command === false) return () => undefined;
47
47
  _storage = storage;
48
- _redactKeys = config.redaction?.body ?? [];
48
+ _redactKeys = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
49
49
  _ignoreRoutes = config.ignoreRoutes;
50
50
  return () => {
51
51
  _storage = null;
@@ -46,7 +46,7 @@ export const HttpClientWatcher: ITraceWatcher & { emit: typeof emit } = Object.f
46
46
  register({ storage, config }: ITraceWatcherConfig): () => void {
47
47
  if (config.watchers.clientRequest === false) return () => undefined;
48
48
  _storage = storage;
49
- _redactHeaderNames = config.redaction?.headers ?? [];
49
+ _redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
50
50
  _ignoreRoutes = config.ignoreRoutes;
51
51
  return () => {
52
52
  _storage = null;
@@ -7,7 +7,7 @@ import { TraceContext } from '../context';
7
7
  import type { ITraceConfig, ITraceWatcher, ITraceWatcherConfig, RequestContent } from '../types';
8
8
  import { EntryType } from '../types';
9
9
  import { AuthTag } from '../utils/authTag';
10
- import { redactHeaders, redactObject } from '../utils/redact';
10
+ import { redactHeaders, redactObject, redactUnknown } from '../utils/redact';
11
11
  import { RequestFilter } from '../utils/requestFilter';
12
12
 
13
13
  const normalizeHeaders = (headers: IRequest['headers']): Record<string, string> => {
@@ -22,15 +22,112 @@ const normalizeHeaders = (headers: IRequest['headers']): Record<string, string>
22
22
  );
23
23
  };
24
24
 
25
+ const normalizeHeaderValue = (value: string | string[]): string => {
26
+ return Array.isArray(value) ? value.join(', ') : value;
27
+ };
28
+
29
+ type ResponseCapture = {
30
+ headers: Record<string, string>;
31
+ body?: unknown;
32
+ restore(): void;
33
+ };
34
+
35
+ type RawResponseWithLifecycle = ReturnType<IResponse['getRaw']> & {
36
+ once?: (event: 'finish' | 'close', listener: () => void) => unknown;
37
+ off?: (event: 'finish' | 'close', listener: () => void) => unknown;
38
+ };
39
+
40
+ const registerCompletionHandler = (response: IResponse, onComplete: () => void): (() => void) => {
41
+ const raw = response.getRaw() as RawResponseWithLifecycle;
42
+ if (typeof raw.once !== 'function') return () => undefined;
43
+
44
+ let completed = false;
45
+
46
+ const cleanup = (): void => {
47
+ if (typeof raw.off === 'function') {
48
+ raw.off('finish', markCompleted);
49
+ raw.off('close', markCompleted);
50
+ }
51
+ };
52
+
53
+ const markCompleted = (): void => {
54
+ if (completed) return;
55
+ completed = true;
56
+ cleanup();
57
+ onComplete();
58
+ };
59
+
60
+ raw.once('finish', markCompleted);
61
+ raw.once('close', markCompleted);
62
+
63
+ return cleanup;
64
+ };
65
+
66
+ const captureResponse = (response: IResponse, config: ITraceConfig): ResponseCapture => {
67
+ const headers: Record<string, string> = {};
68
+ const redactFields = [...config.redaction.keys, ...config.redaction.body];
69
+
70
+ const originalSetHeader = response.setHeader;
71
+ const originalJson = response.json;
72
+ const originalText = response.text;
73
+ const originalHtml = response.html;
74
+ const originalSend = response.send;
75
+
76
+ const capture: ResponseCapture = {
77
+ headers,
78
+ body: undefined,
79
+ restore(): void {
80
+ response.setHeader = originalSetHeader;
81
+ response.json = originalJson;
82
+ response.text = originalText;
83
+ response.html = originalHtml;
84
+ response.send = originalSend;
85
+ },
86
+ };
87
+
88
+ response.setHeader = function setHeader(name: string, value: string | string[]): IResponse {
89
+ headers[name] = normalizeHeaderValue(value);
90
+ return originalSetHeader.call(this, name, value);
91
+ };
92
+
93
+ response.json = function json(data: unknown): void {
94
+ capture.body = redactUnknown(data, redactFields);
95
+ originalJson.call(this, data);
96
+ };
97
+
98
+ response.text = function text(value: string): void {
99
+ capture.body = value;
100
+ originalText.call(this, value);
101
+ };
102
+
103
+ response.html = function html(value: string): void {
104
+ capture.body = value;
105
+ originalHtml.call(this, value);
106
+ };
107
+
108
+ response.send = function send(data: string | Buffer): void {
109
+ capture.body = typeof data === 'string' ? data : `[binary ${data.length} bytes]`;
110
+ originalSend.call(this, data);
111
+ };
112
+
113
+ return capture;
114
+ };
115
+
25
116
  const buildEntry = (
26
117
  req: IRequest,
27
118
  res: IResponse,
28
119
  start: number,
29
- config: ITraceConfig
120
+ config: ITraceConfig,
121
+ responseCapture: ResponseCapture
30
122
  ): RequestContent => {
31
- const headers = redactHeaders(normalizeHeaders(req.headers), config.redaction.headers);
123
+ const headers = redactHeaders(normalizeHeaders(req.headers), [
124
+ ...config.redaction.keys,
125
+ ...config.redaction.headers,
126
+ ]);
32
127
 
33
- const payload = req.body ? redactObject(req.body, config.redaction.body) : {};
128
+ const payload = req.body
129
+ ? redactObject(req.body, [...config.redaction.keys, ...config.redaction.body])
130
+ : {};
34
131
 
35
132
  return {
36
133
  method: req.getMethod(),
@@ -38,7 +135,11 @@ const buildEntry = (
38
135
  headers,
39
136
  payload,
40
137
  responseStatus: res.getStatus(),
41
- responseHeaders: {},
138
+ responseHeaders: redactHeaders(responseCapture.headers, [
139
+ ...config.redaction.keys,
140
+ ...config.redaction.headers,
141
+ ]),
142
+ responseBody: responseCapture.body,
42
143
  duration: Date.now() - start,
43
144
  memory: TraceContext.getMemory(),
44
145
  middleware: [],
@@ -68,24 +169,34 @@ export const HttpWatcher: ITraceWatcher = Object.freeze({
68
169
 
69
170
  const start = TraceContext.now();
70
171
  const batchId = TraceContext.getBatchId();
172
+ const responseCapture = captureResponse(response, config);
173
+ let didPersist = false;
71
174
 
72
- await next();
175
+ const persistEntry = (): void => {
176
+ if (didPersist) return;
177
+ didPersist = true;
73
178
 
74
- const content = buildEntry(request, response, start, config);
75
- const tags = AuthTag.append([]);
76
- if (content.responseStatus >= 500) tags.push('failed');
77
-
78
- storage
79
- .writeEntry({
80
- uuid: crypto.randomUUID(),
81
- batchId,
82
- type: EntryType.REQUEST,
83
- content,
84
- tags,
85
- isLatest: true,
86
- createdAt: TraceContext.now(),
87
- })
88
- .catch(() => undefined); // fire-and-forget
179
+ const content = buildEntry(request, response, start, config, responseCapture);
180
+ const tags = AuthTag.append([]);
181
+ if (content.responseStatus >= 500) tags.push('failed');
182
+
183
+ responseCapture.restore();
184
+
185
+ storage
186
+ .writeEntry({
187
+ uuid: crypto.randomUUID(),
188
+ batchId,
189
+ type: EntryType.REQUEST,
190
+ content,
191
+ tags,
192
+ isLatest: true,
193
+ createdAt: TraceContext.now(),
194
+ })
195
+ .catch(() => undefined); // fire-and-forget
196
+ };
197
+
198
+ registerCompletionHandler(response, persistEntry);
199
+ await next();
89
200
  };
90
201
 
91
202
  registerMiddleware(middleware);
@@ -8,7 +8,7 @@ import { EntryType } from '../types';
8
8
  import { AuthTag } from '../utils/authTag';
9
9
  import { RequestFilter } from '../utils/requestFilter';
10
10
 
11
- type LoggerSink = (level: string, message: string, context?: Record<string, unknown>) => void;
11
+ // type LoggerSink = (level: string, message: string, context?: Record<string, unknown>) => void;
12
12
 
13
13
  const LEVEL_PRIORITY: Record<string, number> = {
14
14
  debug: 0,
@@ -24,37 +24,37 @@ export const LogWatcher: ITraceWatcher = Object.freeze({
24
24
 
25
25
  const minPriority = LEVEL_PRIORITY[config.logMinLevel] ?? 1;
26
26
 
27
- const loggerWithSink = Logger as typeof Logger & {
28
- addSink?: (fn: LoggerSink) => () => void;
29
- };
27
+ const loggerWithSink = Logger;
30
28
 
31
29
  if (typeof loggerWithSink.addSink !== 'function') {
32
30
  return () => undefined;
33
31
  }
34
32
 
35
- const unsubscribe = loggerWithSink.addSink((level, message, context) => {
36
- if ((LEVEL_PRIORITY[level] ?? 0) < minPriority) return;
37
- if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes)) return;
38
-
39
- const content: LogContent = {
40
- level,
41
- message,
42
- context: context ?? undefined,
43
- hostname: TraceContext.getHostname(),
44
- };
45
-
46
- storage
47
- .writeEntry({
48
- uuid: crypto.randomUUID(),
49
- batchId: TraceContext.getBatchId(),
50
- type: EntryType.LOG,
51
- content,
52
- tags: AuthTag.append([]),
53
- isLatest: true,
54
- createdAt: TraceContext.now(),
55
- })
56
- .catch(() => undefined);
57
- });
33
+ const unsubscribe = loggerWithSink.addSink(
34
+ (level: string, message: string, context?: Record<string, unknown>) => {
35
+ if ((LEVEL_PRIORITY[level] ?? 0) < minPriority) return;
36
+ if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes)) return;
37
+
38
+ const content: LogContent = {
39
+ level,
40
+ message,
41
+ context: context ?? undefined,
42
+ hostname: TraceContext.getHostname(),
43
+ };
44
+
45
+ storage
46
+ .writeEntry({
47
+ uuid: crypto.randomUUID(),
48
+ batchId: TraceContext.getBatchId(),
49
+ type: EntryType.LOG,
50
+ content,
51
+ tags: AuthTag.append([]),
52
+ isLatest: true,
53
+ createdAt: TraceContext.now(),
54
+ })
55
+ .catch(() => undefined);
56
+ }
57
+ );
58
58
 
59
59
  return unsubscribe;
60
60
  },