@zintrust/trace 0.4.76 → 0.4.79

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 (47) 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 +88 -29
  6. package/dist/index.d.ts +7 -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 +38 -21
  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/ExceptionWatcher.d.ts +8 -1
  27. package/dist/watchers/ExceptionWatcher.js +12 -7
  28. package/dist/watchers/HttpClientWatcher.js +1 -1
  29. package/dist/watchers/HttpWatcher.js +112 -21
  30. package/package.json +2 -2
  31. package/src/config.ts +152 -5
  32. package/src/dashboard/routes.ts +6 -2
  33. package/src/dashboard/ui.ts +88 -29
  34. package/src/index.ts +10 -0
  35. package/src/register.ts +137 -10
  36. package/src/storage/TraceContentRedaction.ts +44 -0
  37. package/src/storage/TraceEntryFiltering.ts +14 -0
  38. package/src/storage/TraceStorage.ts +52 -5
  39. package/src/storage/TraceWriteDiagnostics.ts +174 -0
  40. package/src/types.ts +41 -21
  41. package/src/utils/entryFilter.ts +108 -0
  42. package/src/utils/redact.ts +57 -9
  43. package/src/watchers/CommandWatcher.ts +1 -1
  44. package/src/watchers/ExceptionWatcher.ts +21 -8
  45. package/src/watchers/HttpClientWatcher.ts +1 -1
  46. package/src/watchers/HttpWatcher.ts +142 -23
  47. package/src/watchers/LogWatcher.ts +26 -28
@@ -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,98 @@ 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 resolveRequestPayload = (req, config) => {
21
+ const redactFields = [...config.redaction.keys, ...config.redaction.body];
22
+ const requestBody = typeof req.getBody === 'function' ? req.getBody() : req.body;
23
+ if (requestBody === undefined || requestBody === null)
24
+ return {};
25
+ if (typeof requestBody === 'object') {
26
+ return redactObject(requestBody, redactFields);
27
+ }
28
+ return redactUnknown(requestBody, redactFields);
29
+ };
30
+ const registerCompletionHandler = (response, onComplete) => {
31
+ const raw = response.getRaw();
32
+ if (typeof raw.once !== 'function')
33
+ return () => undefined;
34
+ let completed = false;
35
+ const cleanup = () => {
36
+ if (typeof raw.off === 'function') {
37
+ raw.off('finish', markCompleted);
38
+ raw.off('close', markCompleted);
39
+ }
40
+ };
41
+ const markCompleted = () => {
42
+ if (completed)
43
+ return;
44
+ completed = true;
45
+ cleanup();
46
+ onComplete();
47
+ };
48
+ raw.once('finish', markCompleted);
49
+ raw.once('close', markCompleted);
50
+ return cleanup;
51
+ };
52
+ const captureResponse = (response, config) => {
53
+ const headers = {};
54
+ const redactFields = [...config.redaction.keys, ...config.redaction.body];
55
+ const originalSetHeader = response.setHeader;
56
+ const originalJson = response.json;
57
+ const originalText = response.text;
58
+ const originalHtml = response.html;
59
+ const originalSend = response.send;
60
+ const capture = {
61
+ headers,
62
+ body: undefined,
63
+ restore() {
64
+ response.setHeader = originalSetHeader;
65
+ response.json = originalJson;
66
+ response.text = originalText;
67
+ response.html = originalHtml;
68
+ response.send = originalSend;
69
+ },
70
+ };
71
+ response.setHeader = function setHeader(name, value) {
72
+ headers[name] = normalizeHeaderValue(value);
73
+ return originalSetHeader.call(this, name, value);
74
+ };
75
+ response.json = function json(data) {
76
+ capture.body = redactUnknown(data, redactFields);
77
+ originalJson.call(this, data);
78
+ };
79
+ response.text = function text(value) {
80
+ capture.body = value;
81
+ originalText.call(this, value);
82
+ };
83
+ response.html = function html(value) {
84
+ capture.body = value;
85
+ originalHtml.call(this, value);
86
+ };
87
+ response.send = function send(data) {
88
+ capture.body = typeof data === 'string' ? data : `[binary ${data.length} bytes]`;
89
+ originalSend.call(this, data);
90
+ };
91
+ return capture;
92
+ };
93
+ const buildEntry = (req, res, start, config, responseCapture) => {
94
+ const headers = redactHeaders(normalizeHeaders(req.headers), [
95
+ ...config.redaction.keys,
96
+ ...config.redaction.headers,
97
+ ]);
20
98
  return {
21
99
  method: req.getMethod(),
22
100
  uri: req.getPath(),
23
101
  headers,
24
- payload,
102
+ payload: resolveRequestPayload(req, config),
25
103
  responseStatus: res.getStatus(),
26
- responseHeaders: {},
104
+ responseHeaders: redactHeaders(responseCapture.headers, [
105
+ ...config.redaction.keys,
106
+ ...config.redaction.headers,
107
+ ]),
108
+ responseBody: responseCapture.body,
27
109
  duration: Date.now() - start,
28
110
  memory: TraceContext.getMemory(),
29
111
  middleware: [],
@@ -48,22 +130,31 @@ export const HttpWatcher = Object.freeze({
48
130
  return next();
49
131
  const start = TraceContext.now();
50
132
  const batchId = TraceContext.getBatchId();
133
+ const responseCapture = captureResponse(response, config);
134
+ let didPersist = false;
135
+ const persistEntry = () => {
136
+ if (didPersist)
137
+ return;
138
+ didPersist = true;
139
+ const content = buildEntry(request, response, start, config, responseCapture);
140
+ const tags = AuthTag.append([]);
141
+ if (content.responseStatus >= 500)
142
+ tags.push('failed');
143
+ responseCapture.restore();
144
+ storage
145
+ .writeEntry({
146
+ uuid: crypto.randomUUID(),
147
+ batchId,
148
+ type: EntryType.REQUEST,
149
+ content,
150
+ tags,
151
+ isLatest: true,
152
+ createdAt: TraceContext.now(),
153
+ })
154
+ .catch(() => undefined); // fire-and-forget
155
+ };
156
+ registerCompletionHandler(response, persistEntry);
51
157
  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
158
  };
68
159
  registerMiddleware(middleware);
69
160
  return () => undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.4.76",
3
+ "version": "0.4.79",
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"
package/src/config.ts CHANGED
@@ -1,7 +1,107 @@
1
1
  /**
2
2
  * TraceConfig — defaults and merge helper for @zintrust/trace
3
3
  */
4
- import type { ITraceConfig, TraceConfigOverrides } from './types';
4
+ import type {
5
+ ITraceConfig,
6
+ TraceConfigOverrides,
7
+ TraceFilterRule,
8
+ TraceRequestWatcherConfig,
9
+ TraceWatcherToggle,
10
+ } from './types';
11
+
12
+ const mergeStringLists = (base: string[], override?: string[]): string[] => {
13
+ const merged = new Set<string>();
14
+
15
+ for (const value of [...base, ...(override ?? [])]) {
16
+ if (typeof value !== 'string') continue;
17
+ const normalized = value.trim();
18
+ if (normalized !== '') merged.add(normalized);
19
+ }
20
+
21
+ return [...merged];
22
+ };
23
+
24
+ const isObjectValue = (value: unknown): value is Record<string, unknown> => {
25
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
26
+ };
27
+
28
+ const mergeFilterRule = (
29
+ base?: TraceFilterRule,
30
+ override?: TraceFilterRule
31
+ ): TraceFilterRule | undefined => {
32
+ const include = mergeStringLists(base?.include ?? [], override?.include);
33
+ const exclude = mergeStringLists(base?.exclude ?? [], override?.exclude);
34
+
35
+ if (include.length === 0 && exclude.length === 0) return undefined;
36
+
37
+ return Object.freeze({
38
+ ...(include.length > 0 ? { include } : {}),
39
+ ...(exclude.length > 0 ? { exclude } : {}),
40
+ });
41
+ };
42
+
43
+ const mergeWatcherToggle = (
44
+ base?: TraceWatcherToggle,
45
+ override?: TraceWatcherToggle
46
+ ): TraceWatcherToggle | undefined => {
47
+ if (override === undefined) return base;
48
+ if (override === false || override === true) return override;
49
+
50
+ const baseRule = isObjectValue(base) ? base : undefined;
51
+ return mergeFilterRule(baseRule, override);
52
+ };
53
+
54
+ const REQUEST_METHOD_KEYS = ['all', 'get', 'post', 'put', 'patch', 'delete'] as const;
55
+
56
+ const mergeRequestWatcherToggle = (
57
+ base?: ITraceConfig['watchers']['request'],
58
+ override?: ITraceConfig['watchers']['request']
59
+ ): ITraceConfig['watchers']['request'] | undefined => {
60
+ if (override === undefined) return base;
61
+ if (override === false || override === true) return override;
62
+
63
+ const baseConfig = isObjectValue(base) ? base : undefined;
64
+ const merged: TraceRequestWatcherConfig = mergeFilterRule(baseConfig, override) ?? {};
65
+
66
+ for (const key of REQUEST_METHOD_KEYS) {
67
+ const rule = mergeFilterRule(baseConfig?.[key], override[key]);
68
+ if (rule !== undefined) merged[key] = rule;
69
+ }
70
+
71
+ return merged;
72
+ };
73
+
74
+ const mergeWatchers = (
75
+ base: ITraceConfig['watchers'],
76
+ override?: TraceConfigOverrides['watchers']
77
+ ): ITraceConfig['watchers'] => {
78
+ if (override === undefined) return { ...base };
79
+
80
+ return {
81
+ ...base,
82
+ ...override,
83
+ request: mergeRequestWatcherToggle(base.request, override.request),
84
+ query: mergeWatcherToggle(base.query, override.query),
85
+ exception: mergeWatcherToggle(base.exception, override.exception),
86
+ log: mergeWatcherToggle(base.log, override.log),
87
+ job: mergeWatcherToggle(base.job, override.job),
88
+ cache: mergeWatcherToggle(base.cache, override.cache),
89
+ schedule: mergeWatcherToggle(base.schedule, override.schedule),
90
+ mail: mergeWatcherToggle(base.mail, override.mail),
91
+ auth: mergeWatcherToggle(base.auth, override.auth),
92
+ event: mergeWatcherToggle(base.event, override.event),
93
+ model: mergeWatcherToggle(base.model, override.model),
94
+ notification: mergeWatcherToggle(base.notification, override.notification),
95
+ redis: mergeWatcherToggle(base.redis, override.redis),
96
+ gate: mergeWatcherToggle(base.gate, override.gate),
97
+ middleware: mergeWatcherToggle(base.middleware, override.middleware),
98
+ command: mergeWatcherToggle(base.command, override.command),
99
+ batch: mergeWatcherToggle(base.batch, override.batch),
100
+ dump: mergeWatcherToggle(base.dump, override.dump),
101
+ view: mergeWatcherToggle(base.view, override.view),
102
+ clientRequest: mergeWatcherToggle(base.clientRequest, override.clientRequest),
103
+ };
104
+ };
5
105
 
6
106
  const DEFAULTS: ITraceConfig = Object.freeze({
7
107
  enabled: false,
@@ -12,6 +112,37 @@ const DEFAULTS: ITraceConfig = Object.freeze({
12
112
  logMinLevel: 'info',
13
113
  watchers: {},
14
114
  redaction: {
115
+ keys: [
116
+ 'password',
117
+ 'pass',
118
+ 'passwd',
119
+ 'token',
120
+ 'accessToken',
121
+ 'access_token',
122
+ 'refreshToken',
123
+ 'refresh_token',
124
+ 'secret',
125
+ 'secretKey',
126
+ 'secret_key',
127
+ 'apiKey',
128
+ 'api_key',
129
+ 'auth',
130
+ 'authToken',
131
+ 'auth_token',
132
+ 'authorization',
133
+ 'cookie',
134
+ 'session',
135
+ 'sessionId',
136
+ 'session_id',
137
+ 'card',
138
+ 'cardNumber',
139
+ 'card_number',
140
+ 'cardToken',
141
+ 'card_token',
142
+ 'cvv',
143
+ 'cvc',
144
+ 'pan',
145
+ ],
15
146
  headers: ['authorization', 'cookie', 'x-api-key', 'x-auth-token'],
16
147
  body: ['password', 'token', 'secret', 'apiKey', 'api_key', 'jwt', 'bearer'],
17
148
  query: [],
@@ -20,7 +151,20 @@ const DEFAULTS: ITraceConfig = Object.freeze({
20
151
 
21
152
  const isWatcherEnabled = (config: ITraceConfig, key: keyof ITraceConfig['watchers']): boolean => {
22
153
  const override = config.watchers[key];
23
- return override !== false; // undefined = enabled by default; explicit false = disabled
154
+ if (override === false) return false;
155
+ if (isObjectValue(override) && override.enabled === false) return false;
156
+ return true; // undefined = enabled by default; explicit false = disabled
157
+ };
158
+
159
+ const getRedactionFields = (
160
+ config: ITraceConfig,
161
+ key: keyof ITraceConfig['redaction']
162
+ ): string[] => {
163
+ if (key === 'keys') {
164
+ return mergeStringLists([], config.redaction.keys);
165
+ }
166
+
167
+ return mergeStringLists(config.redaction.keys, config.redaction[key]);
24
168
  };
25
169
 
26
170
  export const TraceConfig = Object.freeze({
@@ -33,14 +177,17 @@ export const TraceConfig = Object.freeze({
33
177
  return Object.freeze({
34
178
  ...DEFAULTS,
35
179
  ...overrides,
36
- watchers: { ...DEFAULTS.watchers, ...(overrides.watchers ?? {}) },
180
+ watchers: mergeWatchers(DEFAULTS.watchers, overrides.watchers),
37
181
  redaction: {
38
- ...DEFAULTS.redaction,
39
- ...(overrides.redaction ?? {}),
182
+ keys: mergeStringLists(DEFAULTS.redaction.keys, overrides.redaction?.keys),
183
+ headers: mergeStringLists(DEFAULTS.redaction.headers, overrides.redaction?.headers),
184
+ body: mergeStringLists(DEFAULTS.redaction.body, overrides.redaction?.body),
185
+ query: mergeStringLists(DEFAULTS.redaction.query, overrides.redaction?.query),
40
186
  },
41
187
  ignoreRoutes: overrides.ignoreRoutes ?? DEFAULTS.ignoreRoutes,
42
188
  });
43
189
  },
44
190
 
191
+ getRedactionFields,
45
192
  isWatcherEnabled,
46
193
  });
@@ -20,6 +20,10 @@ import {
20
20
  } from './handlers';
21
21
  import { buildDashboardHtml } from './ui';
22
22
 
23
+ type HtmlResponse = {
24
+ html(body: string): void;
25
+ };
26
+
23
27
  export type TraceDashboardOptions = {
24
28
  /** Base path for the dashboard, e.g. '/trace'. Defaults to '/trace'. */
25
29
  basePath?: string;
@@ -61,7 +65,7 @@ export const registerTraceRoutes = (
61
65
  Router.get(
62
66
  router,
63
67
  base,
64
- (_req, res) => {
68
+ (_req: unknown, res: HtmlResponse) => {
65
69
  res.html(buildDashboardHtml(base, appConfig.name));
66
70
  },
67
71
  routeOptions
@@ -70,7 +74,7 @@ export const registerTraceRoutes = (
70
74
  Router.get(
71
75
  router,
72
76
  `${base}/*`,
73
- (_req, res) => {
77
+ (_req: unknown, res: HtmlResponse) => {
74
78
  res.html(buildDashboardHtml(base, appConfig.name));
75
79
  },
76
80
  routeOptions