@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.
- package/README.md +101 -15
- package/dist/build-manifest.json +78 -38
- package/dist/config.d.ts +1 -0
- package/dist/config.js +123 -4
- package/dist/dashboard/ui.js +88 -29
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/migrations/20260331000001_create_zin_trace_entries_table.js +1 -1
- package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.d.ts +10 -0
- package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.js +34 -0
- package/dist/migrations/index.js +2 -1
- package/dist/register.js +107 -9
- package/dist/storage/TraceContentRedaction.d.ts +4 -0
- package/dist/storage/TraceContentRedaction.js +33 -0
- package/dist/storage/TraceEntryFiltering.d.ts +4 -0
- package/dist/storage/TraceEntryFiltering.js +13 -0
- package/dist/storage/TraceStorage.js +35 -5
- package/dist/storage/TraceWriteDiagnostics.d.ts +19 -0
- package/dist/storage/TraceWriteDiagnostics.js +98 -0
- package/dist/types.d.ts +38 -21
- package/dist/utils/entryFilter.d.ts +4 -0
- package/dist/utils/entryFilter.js +95 -0
- package/dist/utils/redact.d.ts +1 -0
- package/dist/utils/redact.js +43 -9
- package/dist/watchers/CommandWatcher.js +1 -1
- package/dist/watchers/ExceptionWatcher.d.ts +8 -1
- package/dist/watchers/ExceptionWatcher.js +12 -7
- package/dist/watchers/HttpClientWatcher.js +1 -1
- package/dist/watchers/HttpWatcher.js +112 -21
- package/package.json +2 -2
- package/src/config.ts +152 -5
- package/src/dashboard/routes.ts +6 -2
- package/src/dashboard/ui.ts +88 -29
- package/src/index.ts +10 -0
- package/src/register.ts +137 -10
- package/src/storage/TraceContentRedaction.ts +44 -0
- package/src/storage/TraceEntryFiltering.ts +14 -0
- package/src/storage/TraceStorage.ts +52 -5
- package/src/storage/TraceWriteDiagnostics.ts +174 -0
- package/src/types.ts +41 -21
- package/src/utils/entryFilter.ts +108 -0
- package/src/utils/redact.ts +57 -9
- package/src/watchers/CommandWatcher.ts +1 -1
- package/src/watchers/ExceptionWatcher.ts +21 -8
- package/src/watchers/HttpClientWatcher.ts +1 -1
- package/src/watchers/HttpWatcher.ts +142 -23
- 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:
|
|
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?:
|
|
285
|
-
query?:
|
|
286
|
-
exception?:
|
|
287
|
-
log?:
|
|
288
|
-
job?:
|
|
289
|
-
cache?:
|
|
290
|
-
schedule?:
|
|
291
|
-
mail?:
|
|
292
|
-
auth?:
|
|
293
|
-
event?:
|
|
294
|
-
model?:
|
|
295
|
-
notification?:
|
|
296
|
-
redis?:
|
|
297
|
-
gate?:
|
|
298
|
-
middleware?:
|
|
299
|
-
command?:
|
|
300
|
-
batch?:
|
|
301
|
-
dump?:
|
|
302
|
-
view?:
|
|
303
|
-
clientRequest?:
|
|
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
|
+
});
|
package/src/utils/redact.ts
CHANGED
|
@@ -2,7 +2,55 @@
|
|
|
2
2
|
* Redaction helpers for @zintrust/trace watchers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
const 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 =
|
|
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
|
|
35
|
-
|
|
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 =
|
|
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 (
|
|
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 & {
|
|
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;
|