@superblocksteam/shared 0.9333.0 → 0.9334.0

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 (33) hide show
  1. package/dist/socket/tracedSocket.d.ts.map +1 -1
  2. package/dist/socket/tracedSocket.js +2 -1
  3. package/dist/socket/tracedSocket.js.map +1 -1
  4. package/dist/tracing/errorSanitizer.d.ts +9 -0
  5. package/dist/tracing/errorSanitizer.d.ts.map +1 -0
  6. package/dist/tracing/errorSanitizer.js +202 -0
  7. package/dist/tracing/errorSanitizer.js.map +1 -0
  8. package/dist/tracing/errorSanitizer.test.d.ts +2 -0
  9. package/dist/tracing/errorSanitizer.test.d.ts.map +1 -0
  10. package/dist/tracing/errorSanitizer.test.js +237 -0
  11. package/dist/tracing/errorSanitizer.test.js.map +1 -0
  12. package/dist/tracing/methodTracing.d.ts.map +1 -1
  13. package/dist/tracing/methodTracing.js +5 -4
  14. package/dist/tracing/methodTracing.js.map +1 -1
  15. package/dist-esm/socket/tracedSocket.d.ts.map +1 -1
  16. package/dist-esm/socket/tracedSocket.js +2 -1
  17. package/dist-esm/socket/tracedSocket.js.map +1 -1
  18. package/dist-esm/tracing/errorSanitizer.d.ts +9 -0
  19. package/dist-esm/tracing/errorSanitizer.d.ts.map +1 -0
  20. package/dist-esm/tracing/errorSanitizer.js +198 -0
  21. package/dist-esm/tracing/errorSanitizer.js.map +1 -0
  22. package/dist-esm/tracing/errorSanitizer.test.d.ts +2 -0
  23. package/dist-esm/tracing/errorSanitizer.test.d.ts.map +1 -0
  24. package/dist-esm/tracing/errorSanitizer.test.js +235 -0
  25. package/dist-esm/tracing/errorSanitizer.test.js.map +1 -0
  26. package/dist-esm/tracing/methodTracing.d.ts.map +1 -1
  27. package/dist-esm/tracing/methodTracing.js +5 -4
  28. package/dist-esm/tracing/methodTracing.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/socket/tracedSocket.ts +2 -1
  31. package/src/tracing/errorSanitizer.test.ts +323 -0
  32. package/src/tracing/errorSanitizer.ts +215 -0
  33. package/src/tracing/methodTracing.ts +5 -4
@@ -0,0 +1,323 @@
1
+ import { sanitizeError } from './errorSanitizer';
2
+
3
+ describe('Error Sanitization', () => {
4
+ it('should redact JWT tokens from error messages', () => {
5
+ const error = new Error(
6
+ 'Invalid token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
7
+ );
8
+
9
+ const sanitized = sanitizeError(error);
10
+
11
+ expect(sanitized.message).toBe('Invalid token: [REDACTED]');
12
+ expect(sanitized.message).not.toContain('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9');
13
+ });
14
+
15
+ it('should redact bearer tokens from error messages', () => {
16
+ const error = new Error('Authentication failed with bearer abc123token456');
17
+
18
+ const sanitized = sanitizeError(error);
19
+
20
+ expect(sanitized.message).toBe('Authentication failed with bearer [REDACTED]');
21
+ });
22
+
23
+ it('should redact API keys from error messages', () => {
24
+ const error = new Error('API request failed: api_key=sk_live_abc123def456');
25
+
26
+ const sanitized = sanitizeError(error);
27
+
28
+ expect(sanitized.message).toBe('API request failed: api_key=[REDACTED]');
29
+ });
30
+
31
+ it('should remove sensitive fields from error objects', () => {
32
+ const error = new Error('Test error') as Error & {
33
+ token?: string;
34
+ apiKey?: string;
35
+ password?: string;
36
+ requestData?: { username: string; authorization: string };
37
+ };
38
+ error.token = 'secret-token-123';
39
+ error.apiKey = 'api-key-456';
40
+ error.password = 'mypassword';
41
+ error.requestData = {
42
+ username: 'john',
43
+ authorization: 'Bearer token123'
44
+ };
45
+
46
+ const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
47
+
48
+ expect(sanitized.token).toBe('[REDACTED]');
49
+ expect(sanitized.apiKey).toBe('[REDACTED]');
50
+ expect(sanitized.password).toBe('[REDACTED]');
51
+ expect((sanitized.requestData as Record<string, unknown>).username).toBe('john'); // Non-sensitive field preserved
52
+ expect((sanitized.requestData as Record<string, unknown>).authorization).toBe('Bearer [REDACTED]');
53
+ });
54
+
55
+ it('should sanitize stack traces', () => {
56
+ const error = new Error('Test error');
57
+ error.stack = `Error: Invalid token eyJhbGciOiJIUzI1NiI...
58
+ at AuthService.validateToken (/app/src/auth.js:42:15)
59
+ at api_key=sk_live_abc123 (/app/src/api.js:20:10)`;
60
+
61
+ const sanitized = sanitizeError(error);
62
+
63
+ expect(sanitized.stack).toContain('[REDACTED]');
64
+ expect(sanitized.stack).not.toContain('eyJhbGciOiJIUzI1NiI');
65
+ expect(sanitized.stack).not.toContain('sk_live_abc123');
66
+ });
67
+
68
+ it('should preserve error type and non-sensitive properties', () => {
69
+ const error = new TypeError('Invalid token: abc123def456ghi789') as TypeError & {
70
+ statusCode?: number;
71
+ userId?: string;
72
+ };
73
+ error.statusCode = 401;
74
+ error.userId = 'user-123';
75
+
76
+ const sanitized = sanitizeError(error) as unknown as Error & Record<string, unknown>;
77
+
78
+ expect(sanitized.name).toBe('TypeError');
79
+ expect(sanitized.statusCode).toBe(401);
80
+ expect(sanitized.userId).toBe('user-123');
81
+ expect(sanitized.message).toBe('Invalid token: [REDACTED]');
82
+ });
83
+
84
+ it('should handle non-Error objects', () => {
85
+ const errorObj = {
86
+ message: 'Auth failed with token abc123',
87
+ token: 'secret-token',
88
+ data: {
89
+ user: 'john',
90
+ password: 'secret123'
91
+ }
92
+ };
93
+
94
+ const sanitized = sanitizeError(errorObj) as unknown as Record<string, unknown>;
95
+
96
+ expect(sanitized.message).toBe('Auth failed with token [REDACTED]');
97
+ expect(sanitized.token).toBe('[REDACTED]');
98
+ expect((sanitized.data as Record<string, unknown>).user).toBe('john');
99
+ expect((sanitized.data as Record<string, unknown>).password).toBe('[REDACTED]');
100
+ });
101
+
102
+ it('should not double-sanitize already sanitized errors', () => {
103
+ const error = new Error('Invalid token: eyJhbGciOiJIUzI1NiI...');
104
+
105
+ const firstSanitization = sanitizeError(error) as Error & { _sanitized?: boolean };
106
+ const secondSanitization = sanitizeError(firstSanitization);
107
+
108
+ expect(firstSanitization.message).toBe(secondSanitization.message);
109
+ expect(firstSanitization._sanitized).toBe(true);
110
+ });
111
+
112
+ it('should handle null and undefined gracefully', () => {
113
+ expect(sanitizeError(null)).toBe(null);
114
+ expect(sanitizeError(undefined)).toBe(undefined);
115
+ });
116
+
117
+ describe('Headers Sanitization', () => {
118
+ it('should sanitize authorization headers while preserving structure', () => {
119
+ const error = new Error('Request failed') as Error & {
120
+ headers?: Record<string, string>;
121
+ };
122
+ error.headers = {
123
+ authorization: 'Bearer eyJhbGciOiJIUzI1NiI...',
124
+ 'content-type': 'application/json',
125
+ 'x-api-key': 'sk_live_abc123'
126
+ };
127
+
128
+ const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
129
+
130
+ expect((sanitized.headers as Record<string, unknown>).authorization).toBe('Bearer [REDACTED]');
131
+ expect((sanitized.headers as Record<string, unknown>)['content-type']).toBe('application/json'); // Non-sensitive preserved
132
+ expect((sanitized.headers as Record<string, unknown>)['x-api-key']).toBe('[REDACTED]'); // Contains 'key' -> completely redacted
133
+ });
134
+
135
+ it('should sanitize headers in HTTP request/response objects', () => {
136
+ const error = new Error('HTTP request failed') as Error & {
137
+ request?: { url: string; method: string; headers: Record<string, string> };
138
+ response?: { status: number; headers: Record<string, string> };
139
+ };
140
+ error.request = {
141
+ url: 'https://api.example.com/users',
142
+ method: 'GET',
143
+ headers: {
144
+ Authorization: 'Bearer token123',
145
+ Cookie: 'session=abc123; auth_token=xyz789',
146
+ 'User-Agent': 'MyApp/1.0'
147
+ }
148
+ };
149
+ error.response = {
150
+ status: 401,
151
+ headers: {
152
+ 'WWW-Authenticate': 'Bearer realm="api"',
153
+ 'Set-Cookie': 'refresh_token=def456; HttpOnly',
154
+ 'Content-Type': 'application/json'
155
+ }
156
+ };
157
+
158
+ const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
159
+
160
+ // Request headers
161
+ expect(((sanitized.request as Record<string, unknown>).headers as Record<string, unknown>).Authorization).toBe('Bearer [REDACTED]');
162
+ expect(((sanitized.request as Record<string, unknown>).headers as Record<string, unknown>).Cookie).toBe('[REDACTED]'); // Contains 'cookie' -> completely redacted
163
+ expect(((sanitized.request as Record<string, unknown>).headers as Record<string, unknown>)['User-Agent']).toBe('MyApp/1.0');
164
+
165
+ // Response headers
166
+ expect(((sanitized.response as Record<string, unknown>).headers as Record<string, unknown>)['WWW-Authenticate']).toBe('[REDACTED]'); // Contains 'bearer' pattern -> redacted
167
+ expect(((sanitized.response as Record<string, unknown>).headers as Record<string, unknown>)['Set-Cookie']).toBe('[REDACTED]'); // Contains 'cookie' -> completely redacted
168
+ expect(((sanitized.response as Record<string, unknown>).headers as Record<string, unknown>)['Content-Type']).toBe('application/json');
169
+ });
170
+
171
+ it('should handle case-insensitive header names', () => {
172
+ const error = new Error('Request failed') as Error & {
173
+ Headers?: Record<string, string>;
174
+ };
175
+ error.Headers = {
176
+ // Capital H - this field name doesn't contain sensitive keywords
177
+ AUTHORIZATION: 'Bearer token123',
178
+ authorization: 'Basic user:pass',
179
+ 'X-API-KEY': 'secret-key'
180
+ };
181
+
182
+ const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
183
+
184
+ // Headers (capital H) is not in VALUE_SANITIZED_FIELDS, so individual fields get processed normally
185
+ // But Basic auth pattern still applies for string value sanitization
186
+ expect((sanitized.Headers as Record<string, unknown>).AUTHORIZATION).toBe('Bearer [REDACTED]'); // Pattern matching on string value
187
+ expect((sanitized.Headers as Record<string, unknown>).authorization).toBe('Basic [REDACTED]'); // Basic auth pattern applies
188
+ expect((sanitized.Headers as Record<string, unknown>)['X-API-KEY']).toBe('[REDACTED]'); // Contains 'key' -> completely redacted
189
+ });
190
+
191
+ it('should sanitize headers with various token formats', () => {
192
+ const errorObj = {
193
+ message: 'Authentication failed',
194
+ headers: {
195
+ authorization: 'JWT eyJhbGciOiJIUzI1NiI...',
196
+ 'x-auth-token': 'Bearer abc123def456',
197
+ 'api-key': 'sk_test_1234567890',
198
+ cookie: 'sessionId=sess_abc123; csrfToken=csrf_xyz789',
199
+ 'x-forwarded-for': '192.168.1.1' // Should be preserved
200
+ }
201
+ };
202
+
203
+ const sanitized = sanitizeError(errorObj) as unknown as Record<string, unknown>;
204
+
205
+ expect((sanitized.headers as Record<string, unknown>).authorization).toBe('JWT [REDACTED]');
206
+ expect((sanitized.headers as Record<string, unknown>)['x-auth-token']).toBe('[REDACTED]'); // Contains 'auth' -> completely redacted
207
+ expect((sanitized.headers as Record<string, unknown>)['api-key']).toBe('[REDACTED]'); // Contains 'key' -> completely redacted
208
+ expect((sanitized.headers as Record<string, unknown>).cookie).toBe('[REDACTED]'); // Contains 'cookie' -> completely redacted
209
+ expect((sanitized.headers as Record<string, unknown>)['x-forwarded-for']).toBe('192.168.1.1');
210
+ });
211
+
212
+ it('should handle nested headers in complex objects', () => {
213
+ const error = new Error('Network error') as Error & {
214
+ requestConfig?: {
215
+ baseURL: string;
216
+ timeout: number;
217
+ headers: Record<string, string>;
218
+ data: { username: string; password: string };
219
+ };
220
+ };
221
+ error.requestConfig = {
222
+ baseURL: 'https://api.example.com',
223
+ timeout: 5000,
224
+ headers: {
225
+ Authorization: 'Bearer eyJhbGciOiJIUzI1NiI...',
226
+ 'Content-Type': 'application/json'
227
+ },
228
+ data: {
229
+ username: 'john',
230
+ password: 'secret123'
231
+ }
232
+ };
233
+
234
+ const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
235
+
236
+ expect((sanitized.requestConfig as Record<string, unknown>).baseURL).toBe('https://api.example.com');
237
+ expect((sanitized.requestConfig as Record<string, unknown>).timeout).toBe(5000);
238
+ expect(((sanitized.requestConfig as Record<string, unknown>).headers as Record<string, unknown>).Authorization).toBe(
239
+ 'Bearer [REDACTED]'
240
+ );
241
+ expect(((sanitized.requestConfig as Record<string, unknown>).headers as Record<string, unknown>)['Content-Type']).toBe(
242
+ 'application/json'
243
+ );
244
+ expect(((sanitized.requestConfig as Record<string, unknown>).data as Record<string, unknown>).username).toBe('john');
245
+ expect(((sanitized.requestConfig as Record<string, unknown>).data as Record<string, unknown>).password).toBe('[REDACTED]');
246
+ });
247
+
248
+ it('should handle headers as strings vs objects', () => {
249
+ const error = new Error('Invalid headers') as Error & {
250
+ headers?: string;
251
+ requestHeaders?: Record<string, string>;
252
+ };
253
+ error.headers = 'Authorization: Bearer token123\r\nContent-Type: application/json';
254
+ error.requestHeaders = {
255
+ authorization: 'Basic dXNlcjpwYXNz', // base64 encoded user:pass
256
+ 'user-agent': 'MyApp/1.0'
257
+ };
258
+
259
+ const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
260
+
261
+ // String headers get pattern-based sanitization (Bearer preserves prefix)
262
+ expect(sanitized.headers).toBe('Authorization: Bearer [REDACTED]\r\nContent-Type: application/json');
263
+
264
+ // Any field named 'authorization' should be value-sanitized regardless of parent object name
265
+ expect((sanitized.requestHeaders as Record<string, unknown>).authorization).toBe('Basic [REDACTED]');
266
+ expect((sanitized.requestHeaders as Record<string, unknown>)['user-agent']).toBe('MyApp/1.0');
267
+ });
268
+
269
+ it('should redact GitHub tokens', () => {
270
+ const errorObj = {
271
+ message: 'GitHub API failed',
272
+ personalToken: 'ghp_1234567890abcdef1234567890abcdef12345678',
273
+ appToken: 'ghs_abcdef1234567890abcdef1234567890abcdef12',
274
+ finegrainedToken: 'github_pat_11ABCDEFG0001234567890_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
275
+ config: {
276
+ auth: 'token ghp_1234567890abcdef1234567890abcdef12345678'
277
+ }
278
+ };
279
+
280
+ const sanitized = sanitizeError(errorObj) as unknown as Record<string, unknown>;
281
+
282
+ expect(sanitized.personalToken).toBe('[REDACTED]');
283
+ expect(sanitized.appToken).toBe('[REDACTED]');
284
+ expect(sanitized.finegrainedToken).toBe('[REDACTED]');
285
+ expect((sanitized.config as Record<string, unknown>).auth).toBe('[REDACTED]'); // "token <value>" pattern matches the whole thing
286
+ });
287
+
288
+ it('should handle mixed secrets in complex error objects', () => {
289
+ const error = new Error('Multiple services failed') as Error & {
290
+ aws?: { accessKeyId: string; secretAccessKey: string; sessionToken: string };
291
+ github?: { token: string };
292
+ database?: { url: string };
293
+ nonSensitive?: { userId: string; timestamp: string };
294
+ };
295
+ error.aws = {
296
+ accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
297
+ secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
298
+ sessionToken: 'aws_session_token_12345abcdef'
299
+ };
300
+ error.github = {
301
+ token: 'ghp_1234567890abcdef1234567890abcdef12345678'
302
+ };
303
+ error.nonSensitive = {
304
+ userId: 'user123',
305
+ timestamp: '2023-01-01T00:00:00Z'
306
+ };
307
+
308
+ const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
309
+
310
+ // AWS credentials should be redacted
311
+ expect((sanitized.aws as Record<string, unknown>).accessKeyId).toBe('[REDACTED]');
312
+ expect((sanitized.aws as Record<string, unknown>).secretAccessKey).toBe('[REDACTED]');
313
+ expect((sanitized.aws as Record<string, unknown>).sessionToken).toBe('[REDACTED]');
314
+
315
+ // GitHub token should be redacted
316
+ expect((sanitized.github as Record<string, unknown>).token).toBe('[REDACTED]');
317
+
318
+ // Non-sensitive data should be preserved
319
+ expect((sanitized.nonSensitive as Record<string, unknown>).userId).toBe('user123');
320
+ expect((sanitized.nonSensitive as Record<string, unknown>).timestamp).toBe('2023-01-01T00:00:00Z');
321
+ });
322
+ });
323
+ });
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Sanitizes errors before recording them in OpenTelemetry spans to prevent
3
+ * sensitive information from being sent to DataDog or other observability platforms.
4
+ */
5
+
6
+ // Patterns that indicate sensitive information
7
+ const SENSITIVE_PATTERNS = [
8
+ // Tokens and keys - capture groups to preserve prefixes
9
+ /(\bbearer\s+)[a-zA-Z0-9\-._~+/]+=*/gi,
10
+ /(\bbasic\s+)\S+/gi, // Basic auth can contain any non-whitespace characters
11
+ /(\bjwt\s+)[a-zA-Z0-9\-._~+/]+=*/gi,
12
+ /(\btoken[:\s=]+)[a-zA-Z0-9\-._~+/]+=*/gi,
13
+ /(\bapi[_\s]?key[:\s=]+)[a-zA-Z0-9\-._~+/]+=*/gi,
14
+ /(\baccess[_\s]?token[:\s=]+)[a-zA-Z0-9\-._~+/]+=*/gi,
15
+ /(\brefresh[_\s]?token[:\s=]+)[a-zA-Z0-9\-._~+/]+=*/gi,
16
+
17
+ // JWT pattern (base64.base64.base64) - standalone tokens
18
+ /\b[A-Za-z0-9-_]{20,}\.[A-Za-z0-9-_]{20,}\.[A-Za-z0-9-_]{20,}\b/g
19
+ ];
20
+
21
+ // Fields to completely remove from error objects
22
+ const COMPLETELY_REDACTED_FIELDS = [
23
+ 'token',
24
+ 'accesstoken',
25
+ 'access_token',
26
+ 'refreshtoken',
27
+ 'refresh_token',
28
+ 'jwt',
29
+ 'bearer',
30
+ 'apikey',
31
+ 'api_key',
32
+ 'secret',
33
+ 'password',
34
+ 'passwd',
35
+ 'key',
36
+ 'private_key',
37
+ 'privatekey',
38
+ 'credentials',
39
+ 'auth',
40
+ 'session',
41
+ 'cookie'
42
+ ];
43
+
44
+ // Fields that should have their values sanitized but structure preserved
45
+ const VALUE_SANITIZED_FIELDS = ['authorization', 'headers'];
46
+
47
+ /**
48
+ * Sanitizes a message string by replacing sensitive patterns
49
+ */
50
+ function sanitizeMessage(message: string): string {
51
+ if (!message) return message;
52
+
53
+ let sanitized = message;
54
+ for (const pattern of SENSITIVE_PATTERNS) {
55
+ if (pattern.global) {
56
+ pattern.lastIndex = 0; // Reset regex state
57
+ }
58
+
59
+ if (pattern.source.includes('(')) {
60
+ // Pattern has capture groups - preserve prefix
61
+ sanitized = sanitized.replace(pattern, '$1[REDACTED]');
62
+ } else {
63
+ // Pattern matches entire sensitive value
64
+ sanitized = sanitized.replace(pattern, '[REDACTED]');
65
+ }
66
+ }
67
+
68
+ return sanitized;
69
+ }
70
+
71
+ /**
72
+ * Sanitizes a stack trace by replacing sensitive patterns
73
+ */
74
+ function sanitizeStackTrace(stack: string): string {
75
+ if (!stack) return stack;
76
+ return sanitizeMessage(stack);
77
+ }
78
+
79
+ /**
80
+ * Recursively sanitizes an object by removing/redacting sensitive fields
81
+ */
82
+ function sanitizeObject(obj: unknown, depth = 0): unknown {
83
+ if (depth > 5) {
84
+ // Prevent infinite recursion
85
+ return '[MAX_DEPTH_REACHED]';
86
+ }
87
+
88
+ if (obj === null || obj === undefined) {
89
+ return obj;
90
+ }
91
+
92
+ if (typeof obj === 'string') {
93
+ return sanitizeMessage(obj);
94
+ }
95
+
96
+ if (typeof obj === 'number' || typeof obj === 'boolean') {
97
+ return obj;
98
+ }
99
+
100
+ if (typeof obj !== 'object') {
101
+ return obj;
102
+ }
103
+
104
+ if (Array.isArray(obj)) {
105
+ return obj.map((item) => sanitizeObject(item, depth + 1));
106
+ }
107
+
108
+ const sanitized: Record<string, unknown> = {};
109
+
110
+ for (const [key, value] of Object.entries(obj)) {
111
+ const lowerKey = key.toLowerCase();
112
+
113
+ // Check for value-sanitized fields first (more specific)
114
+ if (VALUE_SANITIZED_FIELDS.some((field) => lowerKey === field)) {
115
+ if (typeof value === 'string') {
116
+ sanitized[key] = sanitizeMessage(value);
117
+ } else {
118
+ sanitized[key] = sanitizeObject(value, depth + 1);
119
+ }
120
+ continue;
121
+ }
122
+
123
+ // Then check for completely redacted fields
124
+ if (COMPLETELY_REDACTED_FIELDS.some((field) => lowerKey === field || lowerKey.includes(field))) {
125
+ sanitized[key] = '[REDACTED]';
126
+ continue;
127
+ }
128
+
129
+ // Special handling for common error properties
130
+ if (key === 'message') {
131
+ sanitized[key] = sanitizeMessage(value as string);
132
+ } else if (key === 'stack') {
133
+ sanitized[key] = sanitizeStackTrace(value as string);
134
+ } else {
135
+ sanitized[key] = sanitizeObject(value, depth + 1);
136
+ }
137
+ }
138
+
139
+ return sanitized;
140
+ }
141
+
142
+ /**
143
+ * Sanitizes an error object for safe recording in OpenTelemetry spans
144
+ */
145
+ export function sanitizeError(error: unknown): Error {
146
+ if (!error) {
147
+ return error as Error;
148
+ }
149
+
150
+ // If it's already a sanitized error, return as-is
151
+ const errorWithSanitized = error as Error & { _sanitized?: boolean };
152
+ if (errorWithSanitized._sanitized) {
153
+ return errorWithSanitized;
154
+ }
155
+
156
+ let sanitized: Error & { _sanitized?: boolean; [key: string]: unknown };
157
+
158
+ if (error instanceof Error) {
159
+ // Create a new error with sanitized properties
160
+ sanitized = new Error(sanitizeMessage(error.message)) as Error & { _sanitized?: boolean; [key: string]: unknown };
161
+ sanitized.name = error.name;
162
+ sanitized.stack = sanitizeStackTrace(error.stack || '');
163
+
164
+ // Copy and sanitize all enumerable properties
165
+ Object.keys(error).forEach((prop) => {
166
+ if (prop !== 'message' && prop !== 'name' && prop !== 'stack') {
167
+ const lowerProp = prop.toLowerCase();
168
+ const errorWithProps = error as unknown as Record<string, unknown>;
169
+
170
+ // Check for value-sanitized fields first (more specific)
171
+ if (VALUE_SANITIZED_FIELDS.some((field) => lowerProp === field)) {
172
+ if (typeof errorWithProps[prop] === 'string') {
173
+ sanitized[prop] = sanitizeMessage(errorWithProps[prop] as string);
174
+ } else {
175
+ sanitized[prop] = sanitizeObject(errorWithProps[prop]);
176
+ }
177
+ } else if (COMPLETELY_REDACTED_FIELDS.some((field) => lowerProp === field || lowerProp.includes(field))) {
178
+ sanitized[prop] = '[REDACTED]';
179
+ } else {
180
+ sanitized[prop] = sanitizeObject(errorWithProps[prop]);
181
+ }
182
+ }
183
+ });
184
+
185
+ // Also check non-enumerable properties that might have been added
186
+ Object.getOwnPropertyNames(error).forEach((prop) => {
187
+ if (prop !== 'message' && prop !== 'name' && prop !== 'stack' && !Object.prototype.propertyIsEnumerable.call(error, prop)) {
188
+ const lowerProp = prop.toLowerCase();
189
+ const errorWithProps = error as unknown as Record<string, unknown>;
190
+
191
+ // Check for value-sanitized fields first (more specific)
192
+ if (VALUE_SANITIZED_FIELDS.some((field) => lowerProp === field)) {
193
+ if (typeof errorWithProps[prop] === 'string') {
194
+ sanitized[prop] = sanitizeMessage(errorWithProps[prop] as string);
195
+ } else {
196
+ sanitized[prop] = sanitizeObject(errorWithProps[prop]);
197
+ }
198
+ } else if (COMPLETELY_REDACTED_FIELDS.some((field) => lowerProp === field || lowerProp.includes(field))) {
199
+ sanitized[prop] = '[REDACTED]';
200
+ } else {
201
+ sanitized[prop] = sanitizeObject(errorWithProps[prop]);
202
+ }
203
+ }
204
+ });
205
+ } else {
206
+ // For non-Error objects, sanitize the entire object
207
+ const sanitizedObj = sanitizeObject(error);
208
+ sanitized = sanitizedObj as Error & { _sanitized?: boolean; [key: string]: unknown };
209
+ }
210
+
211
+ // Mark as sanitized to prevent double-processing
212
+ sanitized._sanitized = true;
213
+
214
+ return sanitized;
215
+ }
@@ -1,4 +1,5 @@
1
1
  import { Context, context, Span, trace, Tracer } from '@opentelemetry/api';
2
+ import { sanitizeError } from './errorSanitizer';
2
3
 
3
4
  /**
4
5
  * helper function to wrap a class method with tracing
@@ -35,7 +36,7 @@ export function traceMethod(className: string, methodName: string, descriptor: P
35
36
  return res;
36
37
  })
37
38
  .catch((err) => {
38
- span.recordException(err);
39
+ span.recordException(sanitizeError(err));
39
40
  span.end();
40
41
  throw err;
41
42
  });
@@ -45,7 +46,7 @@ export function traceMethod(className: string, methodName: string, descriptor: P
45
46
  span.end();
46
47
  return result;
47
48
  } catch (err) {
48
- span.recordException(err);
49
+ span.recordException(sanitizeError(err));
49
50
  span.end();
50
51
  throw err;
51
52
  }
@@ -81,7 +82,7 @@ export function traceFunction<T>({
81
82
  return res;
82
83
  })
83
84
  .catch((err) => {
84
- span.recordException(err);
85
+ span.recordException(sanitizeError(err));
85
86
  span.end();
86
87
  throw err;
87
88
  });
@@ -90,7 +91,7 @@ export function traceFunction<T>({
90
91
  span.end();
91
92
  return result;
92
93
  } catch (err) {
93
- span.recordException(err);
94
+ span.recordException(sanitizeError(err));
94
95
  span.end();
95
96
  throw err;
96
97
  }