autotel 2.26.0 → 2.26.2
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/dist/attribute-redacting-processor.cjs +14 -6
- package/dist/attribute-redacting-processor.d.cts +63 -1
- package/dist/attribute-redacting-processor.d.ts +63 -1
- package/dist/attribute-redacting-processor.js +1 -1
- package/dist/attributes.cjs +21 -21
- package/dist/attributes.js +2 -2
- package/dist/auto.cjs +8 -8
- package/dist/auto.js +6 -6
- package/dist/{chunk-RUD7KS4R.js → chunk-3SDILILG.js} +3 -3
- package/dist/{chunk-RUD7KS4R.js.map → chunk-3SDILILG.js.map} +1 -1
- package/dist/{chunk-B33XPEKY.js → chunk-55ER2KD5.js} +4 -4
- package/dist/chunk-55ER2KD5.js.map +1 -0
- package/dist/{chunk-UJJPTSEI.cjs → chunk-563EL6O6.cjs} +81 -14
- package/dist/chunk-563EL6O6.cjs.map +1 -0
- package/dist/{chunk-TS7IHIRW.cjs → chunk-6YGUN7IY.cjs} +5 -5
- package/dist/{chunk-TS7IHIRW.cjs.map → chunk-6YGUN7IY.cjs.map} +1 -1
- package/dist/{chunk-XDKK53OL.js → chunk-A4E5AQFK.js} +3 -3
- package/dist/{chunk-XDKK53OL.js.map → chunk-A4E5AQFK.js.map} +1 -1
- package/dist/{chunk-WAB4CHBU.js → chunk-BJ2XPN77.js} +3 -3
- package/dist/{chunk-WAB4CHBU.js.map → chunk-BJ2XPN77.js.map} +1 -1
- package/dist/{chunk-KZEC4CHV.cjs → chunk-CEAQK2QY.cjs} +5 -5
- package/dist/{chunk-KZEC4CHV.cjs.map → chunk-CEAQK2QY.cjs.map} +1 -1
- package/dist/chunk-CMNGGTQL.cjs +349 -0
- package/dist/chunk-CMNGGTQL.cjs.map +1 -0
- package/dist/{chunk-VYA6QDNA.js → chunk-DPSA4QLA.js} +4 -2
- package/dist/chunk-DPSA4QLA.js.map +1 -0
- package/dist/{chunk-M4US3P4K.js → chunk-ER43K7ES.js} +3 -3
- package/dist/{chunk-M4US3P4K.js.map → chunk-ER43K7ES.js.map} +1 -1
- package/dist/{chunk-AZ24DJAG.cjs → chunk-FU6R566Y.cjs} +4 -4
- package/dist/chunk-FU6R566Y.cjs.map +1 -0
- package/dist/{chunk-4PTCDOZY.js → chunk-HPUGKUMZ.js} +4 -4
- package/dist/{chunk-4PTCDOZY.js.map → chunk-HPUGKUMZ.js.map} +1 -1
- package/dist/{chunk-XRBP4RYL.cjs → chunk-JKIMEPI2.cjs} +4 -4
- package/dist/{chunk-XRBP4RYL.cjs.map → chunk-JKIMEPI2.cjs.map} +1 -1
- package/dist/{chunk-N344PVE5.cjs → chunk-OBWXM4NN.cjs} +9 -9
- package/dist/{chunk-N344PVE5.cjs.map → chunk-OBWXM4NN.cjs.map} +1 -1
- package/dist/{chunk-OFPZULMQ.cjs → chunk-OC6X2VIN.cjs} +8 -8
- package/dist/{chunk-OFPZULMQ.cjs.map → chunk-OC6X2VIN.cjs.map} +1 -1
- package/dist/{chunk-GTD3NXOS.js → chunk-QC5MNKVF.js} +4 -4
- package/dist/{chunk-GTD3NXOS.js.map → chunk-QC5MNKVF.js.map} +1 -1
- package/dist/chunk-TDNKIHKT.js +341 -0
- package/dist/chunk-TDNKIHKT.js.map +1 -0
- package/dist/{chunk-DGPUZ6TE.js → chunk-U54FTVFH.js} +3 -3
- package/dist/{chunk-DGPUZ6TE.js.map → chunk-U54FTVFH.js.map} +1 -1
- package/dist/{chunk-ZJ5GXCOT.cjs → chunk-UTZR7P7E.cjs} +36 -36
- package/dist/{chunk-ZJ5GXCOT.cjs.map → chunk-UTZR7P7E.cjs.map} +1 -1
- package/dist/{chunk-7FIGORWI.cjs → chunk-VH77IPJN.cjs} +4 -2
- package/dist/chunk-VH77IPJN.cjs.map +1 -0
- package/dist/{chunk-EXOXDI5A.js → chunk-W35FVJBC.js} +73 -8
- package/dist/chunk-W35FVJBC.js.map +1 -0
- package/dist/{chunk-II7GFVAF.cjs → chunk-WZOKY3PW.cjs} +13 -13
- package/dist/{chunk-II7GFVAF.cjs.map → chunk-WZOKY3PW.cjs.map} +1 -1
- package/dist/{chunk-CMADDTHY.cjs → chunk-YEVCD6DR.cjs} +7 -7
- package/dist/{chunk-CMADDTHY.cjs.map → chunk-YEVCD6DR.cjs.map} +1 -1
- package/dist/{chunk-RXFZKLRQ.js → chunk-YN7USLHW.js} +3 -3
- package/dist/{chunk-RXFZKLRQ.js.map → chunk-YN7USLHW.js.map} +1 -1
- package/dist/decorators.cjs +7 -7
- package/dist/decorators.js +7 -7
- package/dist/event.cjs +10 -10
- package/dist/event.js +7 -7
- package/dist/functional.cjs +14 -14
- package/dist/functional.js +7 -7
- package/dist/index.cjs +340 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +205 -3
- package/dist/index.d.ts +205 -3
- package/dist/index.js +257 -33
- package/dist/index.js.map +1 -1
- package/dist/{init-QSj7X6zU.d.cts → init-CMuTaFAV.d.cts} +26 -1
- package/dist/{init-FiR_glVc.d.ts → init-D6JfWEjL.d.ts} +26 -1
- package/dist/instrumentation.cjs +14 -14
- package/dist/instrumentation.js +6 -6
- package/dist/logger.cjs +8 -8
- package/dist/logger.js +1 -1
- package/dist/messaging.cjs +11 -11
- package/dist/messaging.js +8 -8
- package/dist/metric.cjs +1 -1
- package/dist/metric.js +1 -1
- package/dist/sampling.cjs +15 -15
- package/dist/sampling.js +2 -2
- package/dist/semantic-helpers.cjs +12 -12
- package/dist/semantic-helpers.js +8 -8
- package/dist/tail-sampling-processor.cjs +4 -4
- package/dist/tail-sampling-processor.js +3 -3
- package/dist/testing.cjs +1 -1
- package/dist/testing.js +1 -1
- package/dist/webhook.cjs +9 -8
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.js +8 -7
- package/dist/webhook.js.map +1 -1
- package/dist/workflow-distributed.cjs +9 -9
- package/dist/workflow-distributed.js +7 -7
- package/dist/workflow.cjs +12 -12
- package/dist/workflow.js +8 -8
- package/dist/yaml-config.cjs +6 -6
- package/dist/yaml-config.d.cts +1 -1
- package/dist/yaml-config.d.ts +1 -1
- package/dist/yaml-config.js +3 -3
- package/package.json +1 -1
- package/src/attribute-redacting-processor.test.ts +81 -16
- package/src/attribute-redacting-processor.ts +278 -24
- package/src/autotel-logger.ts +2 -2
- package/src/gen-ai-events.test.ts +135 -0
- package/src/gen-ai-events.ts +199 -0
- package/src/gen-ai-metrics.test.ts +96 -0
- package/src/gen-ai-metrics.ts +128 -0
- package/src/index.ts +28 -1
- package/src/init.ts +117 -2
- package/src/request-logger.test.ts +266 -1
- package/src/request-logger.ts +115 -16
- package/src/structured-error.ts +54 -1
- package/dist/chunk-7FIGORWI.cjs.map +0 -1
- package/dist/chunk-AZ24DJAG.cjs.map +0 -1
- package/dist/chunk-B33XPEKY.js.map +0 -1
- package/dist/chunk-ELW34S4C.cjs +0 -173
- package/dist/chunk-ELW34S4C.cjs.map +0 -1
- package/dist/chunk-EXOXDI5A.js.map +0 -1
- package/dist/chunk-SNINLBEE.js +0 -167
- package/dist/chunk-SNINLBEE.js.map +0 -1
- package/dist/chunk-UJJPTSEI.cjs.map +0 -1
- package/dist/chunk-VYA6QDNA.js.map +0 -1
|
@@ -46,6 +46,11 @@ export type AttributeRedactorFn = (
|
|
|
46
46
|
*/
|
|
47
47
|
export type AttributeRedactorPreset = 'default' | 'strict' | 'pci-dss';
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Masker function type - receives the matched string and returns a masked version
|
|
51
|
+
*/
|
|
52
|
+
export type MaskFn = (match: string) => string;
|
|
53
|
+
|
|
49
54
|
/**
|
|
50
55
|
* Value pattern configuration
|
|
51
56
|
*/
|
|
@@ -56,8 +61,15 @@ export interface ValuePatternConfig {
|
|
|
56
61
|
pattern: RegExp;
|
|
57
62
|
/** Custom replacement (default: uses global replacement) */
|
|
58
63
|
replacement?: string;
|
|
64
|
+
/** Mask function for smart partial masking (overrides replacement) */
|
|
65
|
+
mask?: MaskFn;
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Built-in PII pattern names
|
|
70
|
+
*/
|
|
71
|
+
export type BuiltinPatternName = keyof typeof builtinPatterns;
|
|
72
|
+
|
|
61
73
|
/**
|
|
62
74
|
* Attribute redactor configuration
|
|
63
75
|
*/
|
|
@@ -68,6 +80,15 @@ export interface AttributeRedactorConfig {
|
|
|
68
80
|
/** Patterns to match against attribute values (redacts matched portion) */
|
|
69
81
|
valuePatterns?: ValuePatternConfig[];
|
|
70
82
|
|
|
83
|
+
/** Dot-notation paths to redact (e.g. 'user.password', 'payment.card') */
|
|
84
|
+
paths?: string[];
|
|
85
|
+
|
|
86
|
+
/** Built-in PII patterns to enable. `true` enables all, `false` disables all, array selects specific ones. */
|
|
87
|
+
builtins?: boolean | BuiltinPatternName[];
|
|
88
|
+
|
|
89
|
+
/** Custom RegExp patterns for string-level redaction */
|
|
90
|
+
patterns?: RegExp[];
|
|
91
|
+
|
|
71
92
|
/** Default replacement string (default: '[REDACTED]') */
|
|
72
93
|
replacement?: string;
|
|
73
94
|
|
|
@@ -100,14 +121,112 @@ export const REDACTOR_PATTERNS = {
|
|
|
100
121
|
/^(password|passwd|pwd|secret|token|api[_-]?key|auth|credential|private[_-]?key|authorization)$/i,
|
|
101
122
|
} as const;
|
|
102
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Built-in PII detection patterns with smart masking.
|
|
126
|
+
* Each builtin preserves just enough signal for debugging while scrubbing PII.
|
|
127
|
+
*/
|
|
128
|
+
export const builtinPatterns = {
|
|
129
|
+
/** Credit card numbers → ****1111 (PCI DSS: last 4 allowed) */
|
|
130
|
+
creditCard: {
|
|
131
|
+
pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
|
|
132
|
+
mask: (m: string) => `****${m.replace(/[\s-]/g, '').slice(-4)}`,
|
|
133
|
+
},
|
|
134
|
+
/** Email addresses → a***@***.com */
|
|
135
|
+
email: {
|
|
136
|
+
pattern: /[\w.+-]+@[\w-]+\.[\w.]+/g,
|
|
137
|
+
mask: (m: string) => {
|
|
138
|
+
const at = m.indexOf('@');
|
|
139
|
+
if (at < 1) return '***@***';
|
|
140
|
+
const tld = m.slice(m.lastIndexOf('.'));
|
|
141
|
+
return `${m[0]}***@***${tld}`;
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
/** IPv4 addresses → ***.***.***.100 (last octet only) */
|
|
145
|
+
ipv4: {
|
|
146
|
+
pattern:
|
|
147
|
+
/\b(?!0\.0\.0\.0\b)(?!127\.0\.0\.1\b)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
|
|
148
|
+
mask: (m: string) => `***.***.***.${m.split('.').pop()}`,
|
|
149
|
+
},
|
|
150
|
+
/** International phone numbers → +33******78 (country code + last 2 digits) */
|
|
151
|
+
phone: {
|
|
152
|
+
pattern:
|
|
153
|
+
/(?:\+\d{1,3}[\s.-]?)?\(?\d{1,4}\)?[\s.-]?\d{2,4}[\s.-]?\d{2,4}[\s.-]?\d{2,4}\b/g,
|
|
154
|
+
mask: (m: string) => {
|
|
155
|
+
const digits = m.replace(/[^\d]/g, '');
|
|
156
|
+
const hasPlus = m.startsWith('+');
|
|
157
|
+
if (hasPlus && digits.length > 4) {
|
|
158
|
+
const ccMatch = m.match(/^\+\d{1,3}/);
|
|
159
|
+
const cc = ccMatch ? ccMatch[0] : '+';
|
|
160
|
+
return `${cc}******${digits.slice(-2)}`;
|
|
161
|
+
}
|
|
162
|
+
if (digits.length > 2) {
|
|
163
|
+
return `${'*'.repeat(digits.length - 2)}${digits.slice(-2)}`;
|
|
164
|
+
}
|
|
165
|
+
return '***';
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
/** JWT tokens → eyJ***.*** */
|
|
169
|
+
jwt: {
|
|
170
|
+
pattern: /\beyJ[\w-]*\.[\w-]*\.[\w-]*\b/g,
|
|
171
|
+
mask: () => 'eyJ***.***',
|
|
172
|
+
},
|
|
173
|
+
/** Bearer tokens → Bearer *** */
|
|
174
|
+
bearer: {
|
|
175
|
+
pattern: /\bBearer\s+[\w\-.~+/]{8,}=*/gi,
|
|
176
|
+
mask: () => 'Bearer ***',
|
|
177
|
+
},
|
|
178
|
+
/** IBAN → FR76****189 (country + check digits + last 3) */
|
|
179
|
+
iban: {
|
|
180
|
+
pattern:
|
|
181
|
+
/\b[A-Z]{2}\d{2}[\s-]?[\dA-Z]{4}[\s-]?[\dA-Z]{4}[\s-]?[\dA-Z]{4}[\s-]?[\dA-Z]{0,4}[\s-]?[\dA-Z]{0,4}[\s-]?[\dA-Z]{0,4}\b/g,
|
|
182
|
+
mask: (m: string) => {
|
|
183
|
+
const clean = m.replace(/[\s-]/g, '');
|
|
184
|
+
return `${clean.slice(0, 4)}****${clean.slice(-3)}`;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
} as const;
|
|
188
|
+
|
|
189
|
+
function cloneRegex(re: RegExp): RegExp {
|
|
190
|
+
return new RegExp(re.source, re.flags);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
194
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function toRegExp(value: unknown): RegExp | undefined {
|
|
198
|
+
if (value instanceof RegExp) return value;
|
|
199
|
+
if (typeof value === 'string') return new RegExp(value, 'g');
|
|
200
|
+
if (isPlainObject(value) && typeof value.source === 'string') {
|
|
201
|
+
const flags = typeof value.flags === 'string' ? value.flags : 'g';
|
|
202
|
+
return new RegExp(value.source, flags);
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function toRegExpArray(value: unknown): RegExp[] | undefined {
|
|
208
|
+
if (!Array.isArray(value)) return undefined;
|
|
209
|
+
const out: RegExp[] = [];
|
|
210
|
+
for (const item of value) {
|
|
211
|
+
const re = toRegExp(item);
|
|
212
|
+
if (re) out.push(re);
|
|
213
|
+
}
|
|
214
|
+
return out.length > 0 ? out : [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function builtinToValuePattern(name: BuiltinPatternName): ValuePatternConfig {
|
|
218
|
+
const b = builtinPatterns[name];
|
|
219
|
+
return { name, pattern: cloneRegex(b.pattern), mask: b.mask };
|
|
220
|
+
}
|
|
221
|
+
|
|
103
222
|
/**
|
|
104
223
|
* Default value patterns for the 'default' preset
|
|
105
224
|
*/
|
|
106
225
|
const DEFAULT_VALUE_PATTERNS: ValuePatternConfig[] = [
|
|
107
|
-
|
|
108
|
-
|
|
226
|
+
builtinToValuePattern('email'),
|
|
227
|
+
builtinToValuePattern('phone'),
|
|
109
228
|
{ name: 'ssn', pattern: REDACTOR_PATTERNS.ssn },
|
|
110
|
-
|
|
229
|
+
builtinToValuePattern('creditCard'),
|
|
111
230
|
];
|
|
112
231
|
|
|
113
232
|
/**
|
|
@@ -118,61 +237,158 @@ export const REDACTOR_PRESETS: Record<
|
|
|
118
237
|
AttributeRedactorConfig
|
|
119
238
|
> = {
|
|
120
239
|
/**
|
|
121
|
-
* Default preset - covers common PII patterns
|
|
122
|
-
* Detects: emails, phone numbers, SSNs, credit cards
|
|
240
|
+
* Default preset - covers common PII patterns with smart masking
|
|
241
|
+
* Detects: emails (a***@***.com), phone numbers, SSNs, credit cards (****1111)
|
|
123
242
|
* Redacts keys: password, secret, token, apiKey, auth, credential
|
|
124
243
|
*/
|
|
125
244
|
default: {
|
|
126
245
|
keyPatterns: [REDACTOR_PATTERNS.sensitiveKey],
|
|
127
246
|
valuePatterns: DEFAULT_VALUE_PATTERNS,
|
|
247
|
+
builtins: true,
|
|
128
248
|
replacement: '[REDACTED]',
|
|
129
249
|
},
|
|
130
250
|
|
|
131
251
|
/**
|
|
132
252
|
* Strict preset - more aggressive redaction for high-security environments
|
|
133
|
-
* Includes everything in default plus: Bearer tokens, JWTs, API keys in values
|
|
253
|
+
* Includes everything in default plus: Bearer tokens, JWTs, IBAN, API keys in values
|
|
134
254
|
*/
|
|
135
255
|
strict: {
|
|
136
256
|
keyPatterns: [REDACTOR_PATTERNS.sensitiveKey, /bearer/i, /jwt/i],
|
|
137
257
|
valuePatterns: [
|
|
138
258
|
...DEFAULT_VALUE_PATTERNS,
|
|
139
|
-
|
|
259
|
+
builtinToValuePattern('jwt'),
|
|
260
|
+
builtinToValuePattern('bearer'),
|
|
261
|
+
builtinToValuePattern('iban'),
|
|
140
262
|
{ name: 'apiKeyInValue', pattern: REDACTOR_PATTERNS.apiKeyInValue },
|
|
141
|
-
{ name: 'jwt', pattern: REDACTOR_PATTERNS.jwt },
|
|
142
263
|
],
|
|
264
|
+
builtins: true,
|
|
143
265
|
replacement: '[REDACTED]',
|
|
144
266
|
},
|
|
145
267
|
|
|
146
268
|
/**
|
|
147
269
|
* PCI-DSS preset - focused on payment card industry compliance
|
|
148
|
-
* Redacts: credit card numbers, CVV-like patterns, card-related keys
|
|
270
|
+
* Redacts: credit card numbers (****1111), CVV-like patterns, card-related keys
|
|
149
271
|
*/
|
|
150
272
|
'pci-dss': {
|
|
151
273
|
keyPatterns: [/card/i, /cvv/i, /cvc/i, /pan/i, /expir/i, /ccn/i],
|
|
152
|
-
valuePatterns: [
|
|
153
|
-
|
|
154
|
-
],
|
|
274
|
+
valuePatterns: [builtinToValuePattern('creditCard')],
|
|
275
|
+
builtins: ['creditCard'],
|
|
155
276
|
replacement: '[REDACTED]',
|
|
156
277
|
},
|
|
157
278
|
};
|
|
158
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Normalize redactor config that may have been deserialized from JSON/YAML.
|
|
282
|
+
* Converts regex-like values back to RegExp instances.
|
|
283
|
+
*/
|
|
284
|
+
export function normalizeAttributeRedactorConfig(
|
|
285
|
+
raw: AttributeRedactorConfig | AttributeRedactorPreset | unknown,
|
|
286
|
+
): AttributeRedactorConfig | AttributeRedactorPreset | undefined {
|
|
287
|
+
if (raw === undefined || raw === null) return undefined;
|
|
288
|
+
if (typeof raw === 'string') return raw as AttributeRedactorPreset;
|
|
289
|
+
if (!isPlainObject(raw)) return undefined;
|
|
290
|
+
|
|
291
|
+
const config: AttributeRedactorConfig = {};
|
|
292
|
+
|
|
293
|
+
if (Array.isArray(raw.paths)) {
|
|
294
|
+
config.paths = raw.paths.filter(
|
|
295
|
+
(value): value is string => typeof value === 'string',
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (typeof raw.replacement === 'string') {
|
|
300
|
+
config.replacement = raw.replacement;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (typeof raw.builtins === 'boolean') {
|
|
304
|
+
config.builtins = raw.builtins;
|
|
305
|
+
} else if (Array.isArray(raw.builtins)) {
|
|
306
|
+
config.builtins = raw.builtins.filter(
|
|
307
|
+
(name): name is BuiltinPatternName => typeof name === 'string',
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (typeof raw.redactor === 'function') {
|
|
312
|
+
config.redactor = raw.redactor as AttributeRedactorFn;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const keyPatterns = toRegExpArray(raw.keyPatterns);
|
|
316
|
+
if (keyPatterns) config.keyPatterns = keyPatterns;
|
|
317
|
+
|
|
318
|
+
const patterns = toRegExpArray(raw.patterns);
|
|
319
|
+
if (patterns) config.patterns = patterns;
|
|
320
|
+
|
|
321
|
+
if (Array.isArray(raw.valuePatterns)) {
|
|
322
|
+
const valuePatterns: ValuePatternConfig[] = [];
|
|
323
|
+
for (const item of raw.valuePatterns) {
|
|
324
|
+
if (!isPlainObject(item) || typeof item.name !== 'string') continue;
|
|
325
|
+
const pattern = toRegExp(item.pattern);
|
|
326
|
+
if (!pattern) continue;
|
|
327
|
+
valuePatterns.push({
|
|
328
|
+
name: item.name,
|
|
329
|
+
pattern,
|
|
330
|
+
replacement:
|
|
331
|
+
typeof item.replacement === 'string' ? item.replacement : undefined,
|
|
332
|
+
mask:
|
|
333
|
+
typeof item.mask === 'function' ? (item.mask as MaskFn) : undefined,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
config.valuePatterns = valuePatterns;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return config;
|
|
340
|
+
}
|
|
341
|
+
|
|
159
342
|
/**
|
|
160
343
|
* Resolve config to a normalized form
|
|
161
344
|
*/
|
|
162
345
|
function resolveConfig(
|
|
163
346
|
config: AttributeRedactorConfig | AttributeRedactorPreset,
|
|
164
347
|
): AttributeRedactorConfig {
|
|
165
|
-
|
|
166
|
-
|
|
348
|
+
const normalized = normalizeAttributeRedactorConfig(config);
|
|
349
|
+
if (!normalized) {
|
|
350
|
+
throw new Error('Invalid attribute redactor config');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (typeof normalized === 'string') {
|
|
354
|
+
const preset = REDACTOR_PRESETS[normalized];
|
|
167
355
|
if (!preset) {
|
|
168
356
|
throw new Error(
|
|
169
|
-
`Unknown attribute redactor preset: "${
|
|
357
|
+
`Unknown attribute redactor preset: "${normalized}". ` +
|
|
170
358
|
`Available presets: ${Object.keys(REDACTOR_PRESETS).join(', ')}`,
|
|
171
359
|
);
|
|
172
360
|
}
|
|
173
361
|
return preset;
|
|
174
362
|
}
|
|
175
|
-
|
|
363
|
+
|
|
364
|
+
const resolvedConfig: AttributeRedactorConfig = {
|
|
365
|
+
...normalized,
|
|
366
|
+
keyPatterns: normalized.keyPatterns
|
|
367
|
+
? [...normalized.keyPatterns]
|
|
368
|
+
: undefined,
|
|
369
|
+
valuePatterns: normalized.valuePatterns
|
|
370
|
+
? [...normalized.valuePatterns]
|
|
371
|
+
: undefined,
|
|
372
|
+
paths: normalized.paths ? [...normalized.paths] : undefined,
|
|
373
|
+
patterns: normalized.patterns ? [...normalized.patterns] : undefined,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Merge built-in patterns if enabled
|
|
377
|
+
if (resolvedConfig.builtins !== false) {
|
|
378
|
+
const builtinNames = Array.isArray(resolvedConfig.builtins)
|
|
379
|
+
? resolvedConfig.builtins
|
|
380
|
+
: (Object.keys(builtinPatterns) as BuiltinPatternName[]);
|
|
381
|
+
const builtinValuePatterns = builtinNames
|
|
382
|
+
.filter((name) => name in builtinPatterns)
|
|
383
|
+
.map(builtinToValuePattern);
|
|
384
|
+
|
|
385
|
+
resolvedConfig.valuePatterns = [
|
|
386
|
+
...(resolvedConfig.valuePatterns ?? []),
|
|
387
|
+
...builtinValuePatterns,
|
|
388
|
+
];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return resolvedConfig;
|
|
176
392
|
}
|
|
177
393
|
|
|
178
394
|
/**
|
|
@@ -188,27 +404,40 @@ function createRedactorFromConfig(
|
|
|
188
404
|
|
|
189
405
|
const keyPatterns = config.keyPatterns ?? [];
|
|
190
406
|
const valuePatterns = config.valuePatterns ?? [];
|
|
407
|
+
const paths = config.paths ?? [];
|
|
408
|
+
const pathSet = new Set(paths);
|
|
409
|
+
const customPatterns = config.patterns ?? [];
|
|
191
410
|
const defaultReplacement = config.replacement ?? '[REDACTED]';
|
|
192
411
|
|
|
412
|
+
// Build masker list from valuePatterns that have mask functions
|
|
413
|
+
const maskers: [RegExp, MaskFn][] = valuePatterns
|
|
414
|
+
.filter((vp) => vp.mask)
|
|
415
|
+
.map((vp) => [cloneRegex(vp.pattern), vp.mask!]);
|
|
416
|
+
|
|
193
417
|
return (key: string, value: AttributeValue): AttributeValue => {
|
|
194
418
|
// Check if key matches any sensitive key pattern
|
|
195
419
|
for (const pattern of keyPatterns) {
|
|
196
|
-
// Reset lastIndex for global regexes
|
|
197
420
|
pattern.lastIndex = 0;
|
|
198
421
|
if (pattern.test(key)) {
|
|
199
422
|
return defaultReplacement;
|
|
200
423
|
}
|
|
201
424
|
}
|
|
202
425
|
|
|
203
|
-
//
|
|
426
|
+
// Check if key matches any path-based redaction
|
|
427
|
+
if (pathSet.has(key)) {
|
|
428
|
+
return defaultReplacement;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// For non-string values, return as-is
|
|
204
432
|
if (typeof value !== 'string') {
|
|
205
|
-
// Handle arrays of strings
|
|
206
433
|
if (Array.isArray(value)) {
|
|
207
434
|
return value.map((item) => {
|
|
208
435
|
if (typeof item === 'string') {
|
|
209
436
|
return redactStringValue(
|
|
210
437
|
item,
|
|
211
438
|
valuePatterns,
|
|
439
|
+
maskers,
|
|
440
|
+
customPatterns,
|
|
212
441
|
defaultReplacement,
|
|
213
442
|
) as string;
|
|
214
443
|
}
|
|
@@ -218,25 +447,50 @@ function createRedactorFromConfig(
|
|
|
218
447
|
return value;
|
|
219
448
|
}
|
|
220
449
|
|
|
221
|
-
//
|
|
222
|
-
return redactStringValue(
|
|
450
|
+
// Three-tier strategy: path-based → masker-based → pattern-based
|
|
451
|
+
return redactStringValue(
|
|
452
|
+
value,
|
|
453
|
+
valuePatterns,
|
|
454
|
+
maskers,
|
|
455
|
+
customPatterns,
|
|
456
|
+
defaultReplacement,
|
|
457
|
+
);
|
|
223
458
|
};
|
|
224
459
|
}
|
|
225
460
|
|
|
226
461
|
/**
|
|
227
|
-
* Apply
|
|
462
|
+
* Apply three-tier redaction strategy to a string
|
|
463
|
+
* 1. Masker-based: built-in patterns with smart partial masking
|
|
464
|
+
* 2. Pattern-based: custom RegExp patterns replaced with replacement
|
|
228
465
|
*/
|
|
229
466
|
function redactStringValue(
|
|
230
467
|
value: string,
|
|
231
468
|
patterns: ValuePatternConfig[],
|
|
469
|
+
maskers: [RegExp, MaskFn][],
|
|
470
|
+
customPatterns: RegExp[],
|
|
232
471
|
defaultReplacement: string,
|
|
233
472
|
): string {
|
|
234
473
|
let result = value;
|
|
235
|
-
|
|
236
|
-
|
|
474
|
+
|
|
475
|
+
// Tier 1: Apply maskers (smart partial masking)
|
|
476
|
+
for (const [pattern, mask] of maskers) {
|
|
477
|
+
pattern.lastIndex = 0;
|
|
478
|
+
result = result.replace(pattern, mask);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Tier 2: Apply value patterns without mask (full replacement)
|
|
482
|
+
for (const { pattern, replacement, mask } of patterns) {
|
|
483
|
+
if (mask) continue; // Already handled by maskers
|
|
237
484
|
pattern.lastIndex = 0;
|
|
238
485
|
result = result.replaceAll(pattern, replacement ?? defaultReplacement);
|
|
239
486
|
}
|
|
487
|
+
|
|
488
|
+
// Tier 3: Apply custom patterns
|
|
489
|
+
for (const pattern of customPatterns) {
|
|
490
|
+
pattern.lastIndex = 0;
|
|
491
|
+
result = result.replaceAll(pattern, defaultReplacement);
|
|
492
|
+
}
|
|
493
|
+
|
|
240
494
|
return result;
|
|
241
495
|
}
|
|
242
496
|
|
package/src/autotel-logger.ts
CHANGED
|
@@ -232,7 +232,7 @@ export function createBuiltinLogger(
|
|
|
232
232
|
log(level, extraOrMessage, message as Record<string, unknown>);
|
|
233
233
|
} else {
|
|
234
234
|
// Pure string-only call: logger.info('message')
|
|
235
|
-
log(level, extraOrMessage
|
|
235
|
+
log(level, extraOrMessage);
|
|
236
236
|
}
|
|
237
237
|
} else {
|
|
238
238
|
// Pino style: logger.info({ extra }, 'message')
|
|
@@ -303,7 +303,7 @@ export function createBuiltinLogger(
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
// Pure string-only call: logger.error('message')
|
|
306
|
-
log('error', extraOrMessage
|
|
306
|
+
log('error', extraOrMessage);
|
|
307
307
|
return;
|
|
308
308
|
}
|
|
309
309
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { TraceContext } from './trace-context';
|
|
3
|
+
import {
|
|
4
|
+
recordPromptSent,
|
|
5
|
+
recordResponseReceived,
|
|
6
|
+
recordRetry,
|
|
7
|
+
recordStreamFirstToken,
|
|
8
|
+
recordToolCall,
|
|
9
|
+
} from './gen-ai-events';
|
|
10
|
+
|
|
11
|
+
type CapturedEvent = { name: string; attrs?: Record<string, unknown> };
|
|
12
|
+
|
|
13
|
+
function captureCtx(): {
|
|
14
|
+
ctx: TraceContext;
|
|
15
|
+
events: CapturedEvent[];
|
|
16
|
+
} {
|
|
17
|
+
const events: CapturedEvent[] = [];
|
|
18
|
+
const ctx = {
|
|
19
|
+
addEvent: (name: string, attrs?: Record<string, unknown>) => {
|
|
20
|
+
events.push({ name, attrs });
|
|
21
|
+
},
|
|
22
|
+
setAttribute: () => {},
|
|
23
|
+
setAttributes: () => {},
|
|
24
|
+
setStatus: () => {},
|
|
25
|
+
recordException: () => {},
|
|
26
|
+
addLink: () => {},
|
|
27
|
+
addLinks: () => {},
|
|
28
|
+
updateName: () => {},
|
|
29
|
+
isRecording: () => true,
|
|
30
|
+
end: () => {},
|
|
31
|
+
} as unknown as TraceContext;
|
|
32
|
+
return { ctx, events };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('GenAI span event helpers', () => {
|
|
36
|
+
it('recordPromptSent emits gen_ai.prompt.sent with canonical attrs', () => {
|
|
37
|
+
const { ctx, events } = captureCtx();
|
|
38
|
+
recordPromptSent(ctx, {
|
|
39
|
+
model: 'gpt-4o',
|
|
40
|
+
promptTokens: 1200,
|
|
41
|
+
messageCount: 3,
|
|
42
|
+
operation: 'chat',
|
|
43
|
+
});
|
|
44
|
+
expect(events).toHaveLength(1);
|
|
45
|
+
expect(events[0]).toEqual({
|
|
46
|
+
name: 'gen_ai.prompt.sent',
|
|
47
|
+
attrs: {
|
|
48
|
+
'gen_ai.request.model': 'gpt-4o',
|
|
49
|
+
'gen_ai.usage.input_tokens': 1200,
|
|
50
|
+
'gen_ai.request.message_count': 3,
|
|
51
|
+
'gen_ai.operation.name': 'chat',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('recordPromptSent omits unset fields rather than writing undefined', () => {
|
|
57
|
+
const { ctx, events } = captureCtx();
|
|
58
|
+
recordPromptSent(ctx);
|
|
59
|
+
expect(events[0]?.attrs).toEqual({});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('recordResponseReceived joins finish reasons into a CSV for attribute compat', () => {
|
|
63
|
+
const { ctx, events } = captureCtx();
|
|
64
|
+
recordResponseReceived(ctx, {
|
|
65
|
+
model: 'gpt-4o-2024-11-20',
|
|
66
|
+
promptTokens: 1200,
|
|
67
|
+
completionTokens: 400,
|
|
68
|
+
totalTokens: 1600,
|
|
69
|
+
finishReasons: ['stop', 'tool_calls'],
|
|
70
|
+
});
|
|
71
|
+
expect(events[0]).toEqual({
|
|
72
|
+
name: 'gen_ai.response.received',
|
|
73
|
+
attrs: {
|
|
74
|
+
'gen_ai.response.model': 'gpt-4o-2024-11-20',
|
|
75
|
+
'gen_ai.usage.input_tokens': 1200,
|
|
76
|
+
'gen_ai.usage.output_tokens': 400,
|
|
77
|
+
'gen_ai.usage.total_tokens': 1600,
|
|
78
|
+
'gen_ai.response.finish_reasons': 'stop,tool_calls',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('recordResponseReceived omits finish_reasons when empty', () => {
|
|
84
|
+
const { ctx, events } = captureCtx();
|
|
85
|
+
recordResponseReceived(ctx, { model: 'claude-sonnet-4-6' });
|
|
86
|
+
expect(events[0]?.attrs).not.toHaveProperty(
|
|
87
|
+
'gen_ai.response.finish_reasons',
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('recordRetry captures attempt, reason, delay, and status code', () => {
|
|
92
|
+
const { ctx, events } = captureCtx();
|
|
93
|
+
recordRetry(ctx, {
|
|
94
|
+
attempt: 2,
|
|
95
|
+
reason: 'rate_limit',
|
|
96
|
+
delayMs: 1000,
|
|
97
|
+
statusCode: 429,
|
|
98
|
+
});
|
|
99
|
+
expect(events[0]).toEqual({
|
|
100
|
+
name: 'gen_ai.retry',
|
|
101
|
+
attrs: {
|
|
102
|
+
'retry.attempt': 2,
|
|
103
|
+
'retry.reason': 'rate_limit',
|
|
104
|
+
'retry.delay_ms': 1000,
|
|
105
|
+
'http.response.status_code': 429,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('recordToolCall writes canonical gen_ai.tool.* keys', () => {
|
|
111
|
+
const { ctx, events } = captureCtx();
|
|
112
|
+
recordToolCall(ctx, {
|
|
113
|
+
toolName: 'search_traces',
|
|
114
|
+
toolCallId: 'call-123',
|
|
115
|
+
arguments: '{"serviceName":"api"}',
|
|
116
|
+
});
|
|
117
|
+
expect(events[0]).toEqual({
|
|
118
|
+
name: 'gen_ai.tool.call',
|
|
119
|
+
attrs: {
|
|
120
|
+
'gen_ai.tool.name': 'search_traces',
|
|
121
|
+
'gen_ai.tool.call.id': 'call-123',
|
|
122
|
+
'gen_ai.tool.arguments': '{"serviceName":"api"}',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('recordStreamFirstToken is the bare marker for TTFT', () => {
|
|
128
|
+
const { ctx, events } = captureCtx();
|
|
129
|
+
recordStreamFirstToken(ctx, { tokensSoFar: 1 });
|
|
130
|
+
expect(events[0]).toEqual({
|
|
131
|
+
name: 'gen_ai.stream.first_token',
|
|
132
|
+
attrs: { 'gen_ai.stream.tokens_so_far': 1 },
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|