@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
@@ -0,0 +1,174 @@
1
+ import type { EntryTypeValue, ITraceEntry, ITraceStorage } from '../types';
2
+
3
+ type TraceLogger = {
4
+ warn: (message: string, context?: Record<string, unknown>) => void;
5
+ };
6
+
7
+ type TraceWriteFailureContext = {
8
+ connectionName: string;
9
+ error: unknown;
10
+ operation: string;
11
+ watcherType?: EntryTypeValue;
12
+ };
13
+
14
+ type TraceWriteDiagnosticsSnapshot = {
15
+ degraded: boolean;
16
+ lastErrorMessage: string | null;
17
+ lastFailureAt: number | null;
18
+ totalFailures: number;
19
+ };
20
+
21
+ type TraceWriteDiagnosticsState = TraceWriteDiagnosticsSnapshot & {
22
+ lastLoggedAtByFingerprint: Map<string, number>;
23
+ };
24
+
25
+ const LOG_WINDOW_MS = 30_000;
26
+
27
+ const diagnosticsState: TraceWriteDiagnosticsState = {
28
+ degraded: false,
29
+ lastErrorMessage: null,
30
+ lastFailureAt: null,
31
+ lastLoggedAtByFingerprint: new Map<string, number>(),
32
+ totalFailures: 0,
33
+ };
34
+
35
+ const getErrorMessage = (error: unknown): string => {
36
+ if (error instanceof Error && error.message.trim() !== '') return error.message;
37
+ if (typeof error === 'string' && error.trim() !== '') return error;
38
+
39
+ try {
40
+ const serialized = JSON.stringify(error);
41
+ if (typeof serialized === 'string' && serialized !== '') return serialized;
42
+ } catch {
43
+ // ignore serialization failures
44
+ }
45
+
46
+ return 'Unknown trace storage error';
47
+ };
48
+
49
+ const buildFingerprint = (context: TraceWriteFailureContext): string => {
50
+ return [
51
+ context.connectionName,
52
+ context.operation,
53
+ context.watcherType ?? 'unknown',
54
+ getErrorMessage(context.error),
55
+ ].join('|');
56
+ };
57
+
58
+ const reportFailure = (
59
+ logger: TraceLogger | undefined,
60
+ context: TraceWriteFailureContext
61
+ ): void => {
62
+ const now = Date.now();
63
+ const errorMessage = getErrorMessage(context.error);
64
+ const fingerprint = buildFingerprint(context);
65
+ const lastLoggedAt = diagnosticsState.lastLoggedAtByFingerprint.get(fingerprint);
66
+
67
+ diagnosticsState.degraded = true;
68
+ diagnosticsState.lastErrorMessage = errorMessage;
69
+ diagnosticsState.lastFailureAt = now;
70
+ diagnosticsState.totalFailures += 1;
71
+
72
+ if (!logger) return;
73
+ if (typeof lastLoggedAt === 'number' && now - lastLoggedAt < LOG_WINDOW_MS) return;
74
+
75
+ diagnosticsState.lastLoggedAtByFingerprint.set(fingerprint, now);
76
+ logger.warn('[trace] Trace storage write degraded', {
77
+ connectionName: context.connectionName,
78
+ error: errorMessage,
79
+ lastFailureAt: now,
80
+ operation: context.operation,
81
+ totalFailures: diagnosticsState.totalFailures,
82
+ watcherType: context.watcherType ?? null,
83
+ });
84
+ };
85
+
86
+ const wrapStorageMethod = <TArgs extends unknown[], TResult>(
87
+ method: (...args: TArgs) => Promise<TResult>,
88
+ describeFailure: (...args: TArgs) => Omit<TraceWriteFailureContext, 'connectionName' | 'error'>,
89
+ connectionName: string,
90
+ logger?: TraceLogger
91
+ ): ((...args: TArgs) => Promise<TResult>) => {
92
+ return async (...args: TArgs): Promise<TResult> => {
93
+ try {
94
+ return await method(...args);
95
+ } catch (error) {
96
+ reportFailure(logger, {
97
+ ...describeFailure(...args),
98
+ connectionName,
99
+ error,
100
+ });
101
+ throw error;
102
+ }
103
+ };
104
+ };
105
+
106
+ export const TraceWriteDiagnostics = Object.freeze({
107
+ getSnapshot(): TraceWriteDiagnosticsSnapshot {
108
+ return {
109
+ degraded: diagnosticsState.degraded,
110
+ lastErrorMessage: diagnosticsState.lastErrorMessage,
111
+ lastFailureAt: diagnosticsState.lastFailureAt,
112
+ totalFailures: diagnosticsState.totalFailures,
113
+ };
114
+ },
115
+
116
+ reset(): void {
117
+ diagnosticsState.degraded = false;
118
+ diagnosticsState.lastErrorMessage = null;
119
+ diagnosticsState.lastFailureAt = null;
120
+ diagnosticsState.totalFailures = 0;
121
+ diagnosticsState.lastLoggedAtByFingerprint.clear();
122
+ },
123
+
124
+ wrapStorage(
125
+ storage: ITraceStorage,
126
+ options: { connectionName: string; logger?: TraceLogger }
127
+ ): ITraceStorage {
128
+ return Object.freeze({
129
+ ...storage,
130
+ writeEntry: wrapStorageMethod(
131
+ storage.writeEntry.bind(storage),
132
+ (entry: ITraceEntry) => ({ operation: 'writeEntry', watcherType: entry.type }),
133
+ options.connectionName,
134
+ options.logger
135
+ ),
136
+ updateEntry: wrapStorageMethod(
137
+ storage.updateEntry.bind(storage),
138
+ (_uuid: string, _patch) => ({ operation: 'updateEntry' }),
139
+ options.connectionName,
140
+ options.logger
141
+ ),
142
+ markFamilyStale: wrapStorageMethod(
143
+ storage.markFamilyStale.bind(storage),
144
+ (_familyHash: string, _exceptUuid: string) => ({ operation: 'markFamilyStale' }),
145
+ options.connectionName,
146
+ options.logger
147
+ ),
148
+ prune: wrapStorageMethod(
149
+ storage.prune.bind(storage),
150
+ (_olderThanMs: number, _keepExceptions?: boolean) => ({ operation: 'prune' }),
151
+ options.connectionName,
152
+ options.logger
153
+ ),
154
+ clear: wrapStorageMethod(
155
+ storage.clear.bind(storage),
156
+ () => ({ operation: 'clear' }),
157
+ options.connectionName,
158
+ options.logger
159
+ ),
160
+ addMonitoring: wrapStorageMethod(
161
+ storage.addMonitoring.bind(storage),
162
+ (_tag: string) => ({ operation: 'addMonitoring' }),
163
+ options.connectionName,
164
+ options.logger
165
+ ),
166
+ removeMonitoring: wrapStorageMethod(
167
+ storage.removeMonitoring.bind(storage),
168
+ (_tag: string) => ({ operation: 'removeMonitoring' }),
169
+ options.connectionName,
170
+ options.logger
171
+ ),
172
+ });
173
+ },
174
+ });
package/src/types.ts CHANGED
@@ -41,9 +41,10 @@ export interface RequestContent {
41
41
  method: string;
42
42
  uri: string;
43
43
  headers: Record<string, string>;
44
- payload: Record<string, unknown>;
44
+ payload: unknown;
45
45
  responseStatus: number;
46
46
  responseHeaders: Record<string, string>;
47
+ responseBody?: unknown;
47
48
  duration: number;
48
49
  memory: number | null;
49
50
  middleware: string[];
@@ -275,32 +276,51 @@ export interface ITraceWatcher {
275
276
  // ---------------------------------------------------------------------------
276
277
 
277
278
  export type RedactionConfig = {
279
+ keys: string[];
278
280
  headers: string[];
279
281
  body: string[];
280
282
  query: string[];
281
283
  };
282
284
 
285
+ export type TraceFilterRule = {
286
+ enabled?: boolean;
287
+ include?: string[];
288
+ exclude?: string[];
289
+ };
290
+
291
+ export type TraceRequestWatcherConfig = TraceFilterRule & {
292
+ all?: TraceFilterRule;
293
+ get?: TraceFilterRule;
294
+ post?: TraceFilterRule;
295
+ put?: TraceFilterRule;
296
+ patch?: TraceFilterRule;
297
+ delete?: TraceFilterRule;
298
+ };
299
+
300
+ export type TraceWatcherToggle = boolean | TraceFilterRule;
301
+ export type TraceRequestWatcherToggle = boolean | TraceRequestWatcherConfig;
302
+
283
303
  export type WatcherToggles = {
284
- request?: boolean;
285
- query?: boolean;
286
- exception?: boolean;
287
- log?: boolean;
288
- job?: boolean;
289
- cache?: boolean;
290
- schedule?: boolean;
291
- mail?: boolean;
292
- auth?: boolean;
293
- event?: boolean;
294
- model?: boolean;
295
- notification?: boolean;
296
- redis?: boolean;
297
- gate?: boolean;
298
- middleware?: boolean;
299
- command?: boolean;
300
- batch?: boolean;
301
- dump?: boolean;
302
- view?: boolean;
303
- clientRequest?: boolean;
304
+ request?: TraceRequestWatcherToggle;
305
+ query?: TraceWatcherToggle;
306
+ exception?: TraceWatcherToggle;
307
+ log?: TraceWatcherToggle;
308
+ job?: TraceWatcherToggle;
309
+ cache?: TraceWatcherToggle;
310
+ schedule?: TraceWatcherToggle;
311
+ mail?: TraceWatcherToggle;
312
+ auth?: TraceWatcherToggle;
313
+ event?: TraceWatcherToggle;
314
+ model?: TraceWatcherToggle;
315
+ notification?: TraceWatcherToggle;
316
+ redis?: TraceWatcherToggle;
317
+ gate?: TraceWatcherToggle;
318
+ middleware?: TraceWatcherToggle;
319
+ command?: TraceWatcherToggle;
320
+ batch?: TraceWatcherToggle;
321
+ dump?: TraceWatcherToggle;
322
+ view?: TraceWatcherToggle;
323
+ clientRequest?: TraceWatcherToggle;
304
324
  };
305
325
 
306
326
  export interface ITraceConfig {
@@ -0,0 +1,108 @@
1
+ import type {
2
+ ITraceConfig,
3
+ ITraceEntry,
4
+ TraceFilterRule,
5
+ TraceRequestWatcherConfig,
6
+ WatcherToggles,
7
+ } from '../types';
8
+ import { EntryType } from '../types';
9
+
10
+ const isObjectValue = (value: unknown): value is Record<string, unknown> => {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ };
13
+
14
+ const normalizeTerms = (terms?: string[]): string[] => {
15
+ if (!Array.isArray(terms)) return [];
16
+
17
+ return terms
18
+ .filter((term): term is string => typeof term === 'string')
19
+ .map((term) => term.trim().toLowerCase())
20
+ .filter((term) => term !== '');
21
+ };
22
+
23
+ const matchesRule = (haystack: string, rule?: TraceFilterRule): boolean => {
24
+ if (!rule) return true;
25
+
26
+ const include = normalizeTerms(rule.include);
27
+ const exclude = normalizeTerms(rule.exclude);
28
+
29
+ if (exclude.some((term) => haystack.includes(term))) return false;
30
+ if (include.length === 0) return true;
31
+
32
+ return include.some((term) => haystack.includes(term));
33
+ };
34
+
35
+ const toSearchableText = (entry: ITraceEntry): string => {
36
+ const sections = [entry.type, entry.batchId, ...(entry.tags ?? [])];
37
+
38
+ try {
39
+ sections.push(JSON.stringify(entry.content) ?? '');
40
+ } catch {
41
+ sections.push(String(entry.content ?? ''));
42
+ }
43
+
44
+ return sections.join(' ').toLowerCase();
45
+ };
46
+
47
+ const watcherKeyByEntryType: Record<ITraceEntry['type'], keyof WatcherToggles> = {
48
+ [EntryType.REQUEST]: 'request',
49
+ [EntryType.QUERY]: 'query',
50
+ [EntryType.EXCEPTION]: 'exception',
51
+ [EntryType.LOG]: 'log',
52
+ [EntryType.JOB]: 'job',
53
+ [EntryType.CACHE]: 'cache',
54
+ [EntryType.SCHEDULE]: 'schedule',
55
+ [EntryType.MAIL]: 'mail',
56
+ [EntryType.AUTH]: 'auth',
57
+ [EntryType.EVENT]: 'event',
58
+ [EntryType.MODEL]: 'model',
59
+ [EntryType.NOTIFICATION]: 'notification',
60
+ [EntryType.REDIS]: 'redis',
61
+ [EntryType.GATE]: 'gate',
62
+ [EntryType.MIDDLEWARE]: 'middleware',
63
+ [EntryType.COMMAND]: 'command',
64
+ [EntryType.BATCH]: 'batch',
65
+ [EntryType.DUMP]: 'dump',
66
+ [EntryType.VIEW]: 'view',
67
+ [EntryType.CLIENT_REQUEST]: 'clientRequest',
68
+ };
69
+
70
+ const getRequestMethodRule = (
71
+ watcher: TraceRequestWatcherConfig,
72
+ entry: ITraceEntry
73
+ ): TraceFilterRule | undefined => {
74
+ if (entry.type !== EntryType.REQUEST) return undefined;
75
+
76
+ const content = isObjectValue(entry.content) ? entry.content : undefined;
77
+ const methodValue = content?.['method'];
78
+ const method = typeof methodValue === 'string' ? methodValue.trim().toLowerCase() : '';
79
+
80
+ if (method === 'get') return watcher.get;
81
+ if (method === 'post') return watcher.post;
82
+ if (method === 'put') return watcher.put;
83
+ if (method === 'patch') return watcher.patch;
84
+ if (method === 'delete' || method === 'del') return watcher.delete;
85
+
86
+ return watcher.all;
87
+ };
88
+
89
+ export const TraceEntryFilter = Object.freeze({
90
+ shouldCapture(entry: ITraceEntry, config: ITraceConfig): boolean {
91
+ const watcherKey = watcherKeyByEntryType[entry.type];
92
+ const watcher = config.watchers[watcherKey];
93
+ if (watcher === false) return false;
94
+ if (!isObjectValue(watcher)) return true;
95
+
96
+ const haystack = toSearchableText(entry);
97
+ if (!matchesRule(haystack, watcher)) return false;
98
+
99
+ if (watcherKey === 'request') {
100
+ const requestWatcher = watcher as TraceRequestWatcherConfig;
101
+ const methodRule = getRequestMethodRule(requestWatcher, entry);
102
+ if (!matchesRule(haystack, requestWatcher.all)) return false;
103
+ if (!matchesRule(haystack, methodRule)) return false;
104
+ }
105
+
106
+ return true;
107
+ },
108
+ });
@@ -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;
@@ -12,11 +12,18 @@ import { familyHash } from '../utils/familyHash';
12
12
  import { RequestFilter } from '../utils/requestFilter';
13
13
  import { parseStackFrameLine } from '../utils/stackFrame';
14
14
 
15
+ type ExceptionCaptureContext = {
16
+ batchId?: string;
17
+ hostname?: string;
18
+ path?: string;
19
+ userId?: string;
20
+ };
21
+
15
22
  const getLinePreview = (_file: string, _line: number): Record<string, string> => {
16
23
  return {};
17
24
  };
18
25
 
19
- const buildContent = (err: Error): ExceptionContent => {
26
+ const buildContent = (err: Error, context?: ExceptionCaptureContext): ExceptionContent => {
20
27
  const stack = err.stack ?? '';
21
28
  const trace: ExceptionContent['trace'] = stack
22
29
  .split('\n')
@@ -35,8 +42,8 @@ const buildContent = (err: Error): ExceptionContent => {
35
42
  trace,
36
43
  linePreview: firstFrame ? getLinePreview(firstFrame.file, firstFrame.line) : {},
37
44
  occurrences: 1,
38
- hostname: TraceContext.getHostname(),
39
- userId: TraceContext.getUserId(),
45
+ hostname: context?.hostname ?? TraceContext.getHostname(),
46
+ userId: context?.userId ?? TraceContext.getUserId(),
40
47
  };
41
48
  };
42
49
 
@@ -64,20 +71,24 @@ const unregisterProcessListeners = (): void => {
64
71
  process.off('unhandledRejection', handleUnhandledRejection);
65
72
  };
66
73
 
67
- const captureException = (err: unknown): void => {
74
+ const captureException = (err: unknown, context?: ExceptionCaptureContext): void => {
68
75
  const storage = _storage;
69
76
  if (!storage) return;
70
77
  if (!(err instanceof Error)) return;
71
- if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
78
+ if (context?.path !== undefined) {
79
+ if (RequestFilter.matchesIgnoredPath(context.path, _ignoreRoutes)) return;
80
+ } else if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) {
81
+ return;
82
+ }
72
83
 
73
- const content = buildContent(err);
84
+ const content = buildContent(err, context);
74
85
  const hash = familyHash(`${content.class}:${content.file}:${content.line}`);
75
86
  const uuid = crypto.randomUUID();
76
87
 
77
88
  storage
78
89
  .writeEntry({
79
90
  uuid,
80
- batchId: TraceContext.getBatchId(),
91
+ batchId: context?.batchId ?? TraceContext.getBatchId(),
81
92
  familyHash: hash,
82
93
  type: EntryType.EXCEPTION,
83
94
  content,
@@ -89,7 +100,9 @@ const captureException = (err: unknown): void => {
89
100
  .catch(() => undefined);
90
101
  };
91
102
 
92
- export const ExceptionWatcher: ITraceWatcher & { capture: (err: unknown) => void } = Object.freeze({
103
+ export const ExceptionWatcher: ITraceWatcher & {
104
+ capture: (err: unknown, context?: ExceptionCaptureContext) => void;
105
+ } = Object.freeze({
93
106
  capture: captureException,
94
107
 
95
108
  register({ storage, config }: ITraceWatcherConfig): () => void {
@@ -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;