autotel-edge 3.17.0 → 3.17.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/LICENSE +191 -21
- package/README.md +2 -2
- package/dist/{config-CEF9DMGT.js → config-CFlfvmQ1.js} +2 -2
- package/dist/{config-CEF9DMGT.js.map → config-CFlfvmQ1.js.map} +1 -1
- package/dist/events.js +13 -9
- package/dist/events.js.map +1 -1
- package/dist/{execution-logger-CoxSsBmU.js → execution-logger-CSW2AyVi.js} +7 -4
- package/dist/{execution-logger-CoxSsBmU.js.map → execution-logger-CSW2AyVi.js.map} +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/logger.js +7 -3
- package/dist/logger.js.map +1 -1
- package/package.json +5 -6
- package/src/api/logger.test.ts +0 -1616
- package/src/api/logger.ts +0 -1038
- package/src/api/redact.test.ts +0 -432
- package/src/api/redact.ts +0 -402
- package/src/compose.ts +0 -243
- package/src/composition.test.ts +0 -130
- package/src/composition.ts +0 -132
- package/src/core/buffer.ts +0 -17
- package/src/core/config.test.ts +0 -413
- package/src/core/config.ts +0 -169
- package/src/core/context.ts +0 -245
- package/src/core/exporter.ts +0 -99
- package/src/core/native-bridge.test.ts +0 -176
- package/src/core/native-bridge.ts +0 -278
- package/src/core/provider.context.test.ts +0 -55
- package/src/core/provider.ts +0 -46
- package/src/core/span.ts +0 -222
- package/src/core/spanprocessor.test.ts +0 -521
- package/src/core/spanprocessor.ts +0 -232
- package/src/core/trace-context.ts +0 -95
- package/src/core/tracer.test.ts +0 -123
- package/src/core/tracer.ts +0 -216
- package/src/events/index.test.ts +0 -242
- package/src/events/index.ts +0 -338
- package/src/events.ts +0 -6
- package/src/execution-logger.test.ts +0 -368
- package/src/execution-logger.ts +0 -410
- package/src/functional.test.ts +0 -827
- package/src/functional.ts +0 -1002
- package/src/index.ts +0 -140
- package/src/logger.ts +0 -35
- package/src/middleware-toolkit.test.ts +0 -71
- package/src/middleware-toolkit.ts +0 -94
- package/src/native-routing.test.ts +0 -98
- package/src/parse-error.test.ts +0 -70
- package/src/parse-error.ts +0 -110
- package/src/plugin-runner.test.ts +0 -100
- package/src/plugin-runner.ts +0 -215
- package/src/sampling/index.test.ts +0 -297
- package/src/sampling/index.ts +0 -276
- package/src/sampling.ts +0 -6
- package/src/testing/index.ts +0 -9
- package/src/testing.ts +0 -6
- package/src/types.ts +0 -325
package/src/api/redact.ts
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Redaction utility for structured log output
|
|
3
|
-
*
|
|
4
|
-
* Inspired by pino's redaction — zero-dependency, edge-compatible.
|
|
5
|
-
* Supports deep paths, wildcards, and custom censor values.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* const redactor = createRedactor({
|
|
10
|
-
* paths: ['password', 'user.email', 'headers.authorization'],
|
|
11
|
-
* })
|
|
12
|
-
* const safe = redactor({ password: 'secret', user: { email: 'a@b.com' } })
|
|
13
|
-
* // { password: '[Redacted]', user: { email: '[Redacted]' } }
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const DEFAULT_CENSOR = '[Redacted]';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Validate and parse a dot/bracket path string into segments.
|
|
21
|
-
*
|
|
22
|
-
* Valid: "user.email", "users[*].name", "headers.*", "[0].secret"
|
|
23
|
-
* Invalid: "user..email", "user[", "foo.]bar", ""
|
|
24
|
-
*/
|
|
25
|
-
function parsePath(path: string): string[] {
|
|
26
|
-
if (typeof path !== 'string' || path.length === 0) {
|
|
27
|
-
throw new Error(
|
|
28
|
-
`redact path must be a non-empty string, got: ${JSON.stringify(path)}`,
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Reject obviously malformed paths
|
|
33
|
-
if (path.includes('..')) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
`redact path contains empty segment: ${JSON.stringify(path)}`,
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
// Unmatched brackets: "[" with no "]" or vice versa
|
|
39
|
-
const openBracket = path.indexOf('[');
|
|
40
|
-
const closeBracket = path.indexOf(']');
|
|
41
|
-
if (
|
|
42
|
-
(openBracket !== -1 && closeBracket === -1) ||
|
|
43
|
-
(closeBracket !== -1 && openBracket === -1)
|
|
44
|
-
) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
`redact path has unclosed bracket: ${JSON.stringify(path)}`,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
if (openBracket !== -1 && closeBracket < openBracket) {
|
|
50
|
-
throw new Error(
|
|
51
|
-
`redact path has bracket close before open: ${JSON.stringify(path)}`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
if (/\[\s*\]$/.test(path)) {
|
|
55
|
-
throw new Error(
|
|
56
|
-
`redact path has unclosed bracket: ${JSON.stringify(path)}`,
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
if (/\][^.[\]]/.test(path) && !/\]\[/.test(path)) {
|
|
60
|
-
// e.g. "foo.]bar" — closing bracket not followed by dot, bracket, or end
|
|
61
|
-
const afterClose = path.match(/\]([^.[\]]+)/);
|
|
62
|
-
if (afterClose) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`redact path has invalid characters after ']': ${JSON.stringify(path)}`,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const segments: string[] = [];
|
|
70
|
-
const rx = /[^.[\]]+|\[(\d+|\*)\]/g;
|
|
71
|
-
let match: RegExpExecArray | null;
|
|
72
|
-
let lastIndex = 0;
|
|
73
|
-
while ((match = rx.exec(path)) !== null) {
|
|
74
|
-
segments.push(match[1] ?? match[0]);
|
|
75
|
-
lastIndex = rx.lastIndex;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// If the regex didn't consume the whole string, something is malformed
|
|
79
|
-
if (lastIndex < path.length) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`redact path has unexpected trailing content: ${JSON.stringify(path)}`,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (segments.length === 0) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`redact path produced no segments: ${JSON.stringify(path)}`,
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return segments;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
95
|
-
if (value === null || typeof value !== 'object') return false;
|
|
96
|
-
const proto = Object.getPrototypeOf(value);
|
|
97
|
-
return proto === Object.prototype || proto === null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Deep clone plain objects and arrays only.
|
|
102
|
-
* Non-plain objects (Date, Map, Set, class instances) are returned by reference.
|
|
103
|
-
*/
|
|
104
|
-
function deepClone<T>(value: T): T {
|
|
105
|
-
if (value === null || typeof value !== 'object') return value;
|
|
106
|
-
if (Array.isArray(value)) return value.map((v) => deepClone(v)) as T;
|
|
107
|
-
if (!isPlainObject(value)) return value;
|
|
108
|
-
const result: Record<string, unknown> = {};
|
|
109
|
-
for (const key of Object.keys(value)) {
|
|
110
|
-
result[key] = deepClone((value as Record<string, unknown>)[key]);
|
|
111
|
-
}
|
|
112
|
-
return result as T;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* A node in the compiled path tree.
|
|
117
|
-
*
|
|
118
|
-
* Unlike a simple PathTree | null representation, a node can be both
|
|
119
|
-
* a redact target AND a parent for deeper paths. This correctly handles
|
|
120
|
-
* overlapping paths like ['user', 'user.email'].
|
|
121
|
-
*/
|
|
122
|
-
interface PathNode {
|
|
123
|
-
redact?: boolean;
|
|
124
|
-
children?: Record<string, PathNode>;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Insert a parsed path into the tree.
|
|
129
|
-
*
|
|
130
|
-
* buildPathTree([['user'], ['user', 'email']]) produces:
|
|
131
|
-
* { user: { redact: true, children: { email: { redact: true } } } }
|
|
132
|
-
*/
|
|
133
|
-
function buildPathTree(paths: string[][]): PathNode {
|
|
134
|
-
const root: PathNode = {};
|
|
135
|
-
for (const segments of paths) {
|
|
136
|
-
let node = root;
|
|
137
|
-
for (let i = 0; i < segments.length; i++) {
|
|
138
|
-
const seg = segments[i];
|
|
139
|
-
const isLast = i === segments.length - 1;
|
|
140
|
-
|
|
141
|
-
if (!node.children) node.children = {};
|
|
142
|
-
if (!node.children[seg]) node.children[seg] = {};
|
|
143
|
-
|
|
144
|
-
if (isLast) {
|
|
145
|
-
node.children[seg].redact = true;
|
|
146
|
-
}
|
|
147
|
-
node = node.children[seg];
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return root;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Walk a compiled path tree and redact matching values in `obj`.
|
|
155
|
-
* Mutates `obj` in place (caller should clone before calling).
|
|
156
|
-
*/
|
|
157
|
-
function redactWithTree(
|
|
158
|
-
obj: Record<string, any>,
|
|
159
|
-
node: PathNode,
|
|
160
|
-
censor: Censor,
|
|
161
|
-
remove: boolean,
|
|
162
|
-
path: string[] = [],
|
|
163
|
-
): void {
|
|
164
|
-
if (!node.children) return;
|
|
165
|
-
|
|
166
|
-
const applyCensor = (target: Record<string, any>, k: string, currentPath: string[]) => {
|
|
167
|
-
if (remove) {
|
|
168
|
-
delete target[k];
|
|
169
|
-
} else {
|
|
170
|
-
target[k] = typeof censor === 'function' ? censor(target[k], currentPath) : censor;
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
for (const [key, childNode] of Object.entries(node.children)) {
|
|
175
|
-
if (key === '*') {
|
|
176
|
-
for (const k of Object.keys(obj)) {
|
|
177
|
-
const wildcardPath = [...path, k];
|
|
178
|
-
if (childNode.redact && !childNode.children) {
|
|
179
|
-
applyCensor(obj, k, wildcardPath);
|
|
180
|
-
} else if (childNode.redact && childNode.children) {
|
|
181
|
-
const child = obj[k];
|
|
182
|
-
if (
|
|
183
|
-
child != null &&
|
|
184
|
-
typeof child === 'object' &&
|
|
185
|
-
!Array.isArray(child)
|
|
186
|
-
) {
|
|
187
|
-
redactWithTree(child, childNode, censor, remove, wildcardPath);
|
|
188
|
-
} else {
|
|
189
|
-
applyCensor(obj, k, wildcardPath);
|
|
190
|
-
}
|
|
191
|
-
} else if (childNode.children) {
|
|
192
|
-
const child = obj[k];
|
|
193
|
-
if (child != null && typeof child === 'object') {
|
|
194
|
-
redactWithTree(child, childNode, censor, remove, wildcardPath);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
202
|
-
|
|
203
|
-
const val = obj[key];
|
|
204
|
-
const currentPath = [...path, key];
|
|
205
|
-
|
|
206
|
-
if (childNode.redact && !childNode.children) {
|
|
207
|
-
applyCensor(obj, key, currentPath);
|
|
208
|
-
} else if (childNode.redact && childNode.children) {
|
|
209
|
-
if (val != null && typeof val === 'object') {
|
|
210
|
-
redactWithTree(val, childNode, censor, remove, currentPath);
|
|
211
|
-
}
|
|
212
|
-
} else if (childNode.children && val != null && typeof val === 'object') {
|
|
213
|
-
redactWithTree(val, childNode, censor, remove, currentPath);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export type Censor = string | ((value: unknown, path: string[]) => unknown);
|
|
219
|
-
|
|
220
|
-
export interface RedactorOptions {
|
|
221
|
-
paths: string[];
|
|
222
|
-
censor?: Censor;
|
|
223
|
-
remove?: boolean;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Preset name or explicit options
|
|
228
|
-
*/
|
|
229
|
-
export type RedactorConfig = RedactorOptions | RedactorPreset;
|
|
230
|
-
|
|
231
|
-
export type RedactorPreset = 'default' | 'strict' | 'pci-dss';
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Paths that match the main autotel package's `sensitiveKey` pattern:
|
|
235
|
-
* `/^(password|passwd|pwd|secret|token|api[_-]?key|auth|credential|private[_-]?key|authorization)$/i`
|
|
236
|
-
*
|
|
237
|
-
* Copied here so the edge logger stays zero-dependency.
|
|
238
|
-
*/
|
|
239
|
-
const SENSITIVE_KEY_PATHS = [
|
|
240
|
-
'password',
|
|
241
|
-
'passwd',
|
|
242
|
-
'pwd',
|
|
243
|
-
'secret',
|
|
244
|
-
'token',
|
|
245
|
-
'apiKey',
|
|
246
|
-
'api_key',
|
|
247
|
-
'auth',
|
|
248
|
-
'credential',
|
|
249
|
-
'privateKey',
|
|
250
|
-
'private_key',
|
|
251
|
-
'authorization',
|
|
252
|
-
];
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Common request header paths that often carry secrets.
|
|
256
|
-
*/
|
|
257
|
-
const SENSITIVE_HEADER_PATHS = [
|
|
258
|
-
'req.headers.authorization',
|
|
259
|
-
'req.headers.cookie',
|
|
260
|
-
'headers.authorization',
|
|
261
|
-
'headers.cookie',
|
|
262
|
-
'request.headers.authorization',
|
|
263
|
-
'request.headers.cookie',
|
|
264
|
-
];
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Built-in redactor presets.
|
|
268
|
-
*
|
|
269
|
-
* - `default` — sensitive keys + request headers (passwords, tokens, secrets, auth headers)
|
|
270
|
-
* - `strict` — default + bearer/jwt/api-key patterns nested at any depth
|
|
271
|
-
* - `pci-dss` — credit card and payment-related fields
|
|
272
|
-
*/
|
|
273
|
-
export const REDACT_PRESETS: Record<RedactorPreset, RedactorOptions> = {
|
|
274
|
-
/**
|
|
275
|
-
* Default: covers the same sensitive-key names as the main autotel package's
|
|
276
|
-
* `sensitiveKey` pattern, plus common request header paths.
|
|
277
|
-
*
|
|
278
|
-
* Redacted keys: password, passwd, pwd, secret, token, apiKey, auth,
|
|
279
|
-
* credential, privateKey, authorization
|
|
280
|
-
* Redacted headers: req.headers.authorization, req.headers.cookie, etc.
|
|
281
|
-
*/
|
|
282
|
-
default: {
|
|
283
|
-
paths: [...SENSITIVE_KEY_PATHS, ...SENSITIVE_HEADER_PATHS],
|
|
284
|
-
},
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Strict: everything in default, plus nested paths for bearer tokens,
|
|
288
|
-
* JWT fields, and API key fields at common depths.
|
|
289
|
-
*/
|
|
290
|
-
strict: {
|
|
291
|
-
paths: [
|
|
292
|
-
...SENSITIVE_KEY_PATHS,
|
|
293
|
-
...SENSITIVE_HEADER_PATHS,
|
|
294
|
-
// Common nested auth shapes
|
|
295
|
-
'bearer',
|
|
296
|
-
'jwt',
|
|
297
|
-
'apiSecret',
|
|
298
|
-
'api_secret',
|
|
299
|
-
'accessToken',
|
|
300
|
-
'access_token',
|
|
301
|
-
'refreshToken',
|
|
302
|
-
'refresh_token',
|
|
303
|
-
'clientSecret',
|
|
304
|
-
'client_secret',
|
|
305
|
-
],
|
|
306
|
-
},
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* PCI-DSS: credit card and payment-related fields.
|
|
310
|
-
*/
|
|
311
|
-
'pci-dss': {
|
|
312
|
-
paths: [
|
|
313
|
-
'card',
|
|
314
|
-
'cardNumber',
|
|
315
|
-
'card_number',
|
|
316
|
-
'ccn',
|
|
317
|
-
'pan',
|
|
318
|
-
'cvv',
|
|
319
|
-
'cvc',
|
|
320
|
-
'expirationDate',
|
|
321
|
-
'expiration_date',
|
|
322
|
-
'exp',
|
|
323
|
-
'payment.card',
|
|
324
|
-
'payment.cardNumber',
|
|
325
|
-
'payment.cvv',
|
|
326
|
-
],
|
|
327
|
-
},
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Resolve a preset name or explicit options into RedactorOptions.
|
|
332
|
-
*/
|
|
333
|
-
function resolveConfig(config: RedactorConfig): RedactorOptions {
|
|
334
|
-
if (typeof config === 'string') {
|
|
335
|
-
const preset = REDACT_PRESETS[config];
|
|
336
|
-
if (!preset) {
|
|
337
|
-
throw new Error(
|
|
338
|
-
`Unknown redactor preset: "${config}". Available: ${Object.keys(REDACT_PRESETS).join(', ')}`,
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
return preset;
|
|
342
|
-
}
|
|
343
|
-
return config;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Create a redactor function from a list of paths.
|
|
348
|
-
*
|
|
349
|
-
* Paths use dot notation for nested fields and `[*]` or `*` for wildcards:
|
|
350
|
-
* - `"password"` — top-level key
|
|
351
|
-
* - `"user.email"` — nested field
|
|
352
|
-
* - `"users[*].password"` or `"users.*.password"` — all items in array/object
|
|
353
|
-
* - `"headers.authorization"` — nested header
|
|
354
|
-
* - `"[*].secret"` — wildcard at root
|
|
355
|
-
*
|
|
356
|
-
* Overlapping paths are supported — e.g. `['user', 'user.email']` will
|
|
357
|
-
* redact `user` as a whole while still recursing into `user.email`.
|
|
358
|
-
*
|
|
359
|
-
* The returned function clones plain objects/arrays before mutating,
|
|
360
|
-
* so the original is never modified. Non-plain objects (Date, Map, etc.)
|
|
361
|
-
* are passed through by reference.
|
|
362
|
-
*
|
|
363
|
-
* @example
|
|
364
|
-
* ```typescript
|
|
365
|
-
* // Using a preset
|
|
366
|
-
* const redactor = createRedactor('default')
|
|
367
|
-
*
|
|
368
|
-
* // Using explicit paths
|
|
369
|
-
* const redactor = createRedactor({
|
|
370
|
-
* paths: ['password', 'token', 'user.email', 'users[*].ssn'],
|
|
371
|
-
* censor: '[Filtered]', // optional, default '[Redacted]'
|
|
372
|
-
* })
|
|
373
|
-
*
|
|
374
|
-
* redactor({ password: 's3cret', user: { email: 'a@b.com' } })
|
|
375
|
-
* // → { password: '[Filtered]', user: { email: '[Filtered]' } }
|
|
376
|
-
*
|
|
377
|
-
* // Custom censor function:
|
|
378
|
-
* const mask = createRedactor({
|
|
379
|
-
* paths: ['ccn'],
|
|
380
|
-
* censor: (val) => '****' + String(val).slice(-4),
|
|
381
|
-
* })
|
|
382
|
-
* ```
|
|
383
|
-
*/
|
|
384
|
-
export function createRedactor(config: RedactorConfig): <T>(obj: T) => T {
|
|
385
|
-
const { paths, censor = DEFAULT_CENSOR, remove = false } = resolveConfig(config);
|
|
386
|
-
|
|
387
|
-
if (paths.length === 0) {
|
|
388
|
-
return <T>(obj: T): T => obj;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const parsed = paths.map((p) => parsePath(p));
|
|
392
|
-
const tree = buildPathTree(parsed);
|
|
393
|
-
|
|
394
|
-
return function redactor<T>(obj: T): T {
|
|
395
|
-
if (obj == null || typeof obj !== 'object') return obj;
|
|
396
|
-
|
|
397
|
-
// Clone so we don't mutate the original log entry
|
|
398
|
-
const clone = deepClone(obj) as Record<string, any>;
|
|
399
|
-
redactWithTree(clone, tree, censor, remove);
|
|
400
|
-
return clone as T;
|
|
401
|
-
};
|
|
402
|
-
}
|
package/src/compose.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Composition utilities for autotel-edge
|
|
3
|
-
*
|
|
4
|
-
* Helper functions for composing instrumentation and middleware.
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```typescript
|
|
8
|
-
* import { compose } from 'autotel-edge/api/compose'
|
|
9
|
-
* import { instrumentGlobalFetch, instrumentGlobalCache } from 'autotel-edge/instrumentation'
|
|
10
|
-
*
|
|
11
|
-
* const setupInstrumentation = compose(
|
|
12
|
-
* instrumentGlobalFetch,
|
|
13
|
-
* instrumentGlobalCache
|
|
14
|
-
* )
|
|
15
|
-
*
|
|
16
|
-
* setupInstrumentation({ enabled: true })
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Compose multiple setup functions into one
|
|
22
|
-
*
|
|
23
|
-
* Takes multiple instrumentation functions and returns a single function
|
|
24
|
-
* that calls them all in order with the same config.
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```typescript
|
|
28
|
-
* const setup = compose(
|
|
29
|
-
* instrumentGlobalFetch,
|
|
30
|
-
* instrumentGlobalCache
|
|
31
|
-
* )
|
|
32
|
-
*
|
|
33
|
-
* setup({ enabled: true })
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
export function compose<TConfig = unknown>(
|
|
37
|
-
...fns: Array<(config?: TConfig) => void>
|
|
38
|
-
): (config?: TConfig) => void {
|
|
39
|
-
return (config?: TConfig) => {
|
|
40
|
-
for (const fn of fns) {
|
|
41
|
-
fn(config);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Compose multiple async setup functions into one
|
|
48
|
-
*
|
|
49
|
-
* Like `compose` but for async functions.
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* ```typescript
|
|
53
|
-
* const setup = composeAsync(
|
|
54
|
-
* async (config) => { await initTracing(config) },
|
|
55
|
-
* async (config) => { await initMetrics(config) }
|
|
56
|
-
* )
|
|
57
|
-
*
|
|
58
|
-
* await setup({ endpoint: 'https://...' })
|
|
59
|
-
* ```
|
|
60
|
-
*/
|
|
61
|
-
export function composeAsync<TConfig = unknown>(
|
|
62
|
-
...fns: Array<(config?: TConfig) => Promise<void>>
|
|
63
|
-
): (config?: TConfig) => Promise<void> {
|
|
64
|
-
return async (config?: TConfig) => {
|
|
65
|
-
for (const fn of fns) {
|
|
66
|
-
await fn(config);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Pipe - compose functions from left to right
|
|
73
|
-
*
|
|
74
|
-
* Unlike `compose` which is right-to-left, pipe is left-to-right
|
|
75
|
-
* which matches the execution order visually.
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* ```typescript
|
|
79
|
-
* const setup = pipe(
|
|
80
|
-
* (config) => ({ ...config, tracing: true }),
|
|
81
|
-
* (config) => ({ ...config, metrics: true }),
|
|
82
|
-
* (config) => initObservability(config)
|
|
83
|
-
* )
|
|
84
|
-
*
|
|
85
|
-
* setup({ service: 'my-worker' })
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
|
-
export function pipe<TInput, TOutput>(
|
|
89
|
-
...fns: Array<(input: any) => any>
|
|
90
|
-
): (input: TInput) => TOutput {
|
|
91
|
-
return (input: TInput) => {
|
|
92
|
-
return fns.reduce((acc, fn) => fn(acc), input as any) as TOutput;
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Create a conditional instrumentation function
|
|
98
|
-
*
|
|
99
|
-
* Only runs the instrumentation if the predicate returns true.
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* ```typescript
|
|
103
|
-
* const setupFetch = when(
|
|
104
|
-
* (env) => env.ENABLE_FETCH_TRACING === 'true',
|
|
105
|
-
* instrumentGlobalFetch
|
|
106
|
-
* )
|
|
107
|
-
*
|
|
108
|
-
* setupFetch(env)
|
|
109
|
-
* ```
|
|
110
|
-
*/
|
|
111
|
-
export function when<TConfig = unknown>(
|
|
112
|
-
predicate: (config?: TConfig) => boolean,
|
|
113
|
-
fn: (config?: TConfig) => void
|
|
114
|
-
): (config?: TConfig) => void {
|
|
115
|
-
return (config?: TConfig) => {
|
|
116
|
-
if (predicate(config)) {
|
|
117
|
-
fn(config);
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Create a conditional async instrumentation function
|
|
124
|
-
*
|
|
125
|
-
* @example
|
|
126
|
-
* ```typescript
|
|
127
|
-
* const setupCache = whenAsync(
|
|
128
|
-
* async (env) => await featureEnabled('cache-tracing'),
|
|
129
|
-
* instrumentGlobalCache
|
|
130
|
-
* )
|
|
131
|
-
*
|
|
132
|
-
* await setupCache(env)
|
|
133
|
-
* ```
|
|
134
|
-
*/
|
|
135
|
-
export function whenAsync<TConfig = unknown>(
|
|
136
|
-
predicate: (config?: TConfig) => Promise<boolean> | boolean,
|
|
137
|
-
fn: (config?: TConfig) => Promise<void>
|
|
138
|
-
): (config?: TConfig) => Promise<void> {
|
|
139
|
-
return async (config?: TConfig) => {
|
|
140
|
-
if (await predicate(config)) {
|
|
141
|
-
await fn(config);
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Tap - run a side effect and return the original value
|
|
148
|
-
*
|
|
149
|
-
* Useful for logging or debugging in a pipe.
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* ```typescript
|
|
153
|
-
* const setup = pipe(
|
|
154
|
-
* tap((config) => console.log('Initial config:', config)),
|
|
155
|
-
* (config) => ({ ...config, tracing: true }),
|
|
156
|
-
* tap((config) => console.log('After tracing:', config)),
|
|
157
|
-
* (config) => initObservability(config)
|
|
158
|
-
* )
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
export function tap<T>(fn: (value: T) => void): (value: T) => T {
|
|
162
|
-
return (value: T) => {
|
|
163
|
-
fn(value);
|
|
164
|
-
return value;
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Memoize - cache the result of a function
|
|
170
|
-
*
|
|
171
|
-
* Useful for expensive initialization functions that should only run once.
|
|
172
|
-
*
|
|
173
|
-
* @example
|
|
174
|
-
* ```typescript
|
|
175
|
-
* const setup = memoize(() => {
|
|
176
|
-
* console.log('Setting up (expensive)...')
|
|
177
|
-
* instrumentGlobalFetch()
|
|
178
|
-
* instrumentGlobalCache()
|
|
179
|
-
* })
|
|
180
|
-
*
|
|
181
|
-
* setup() // Logs "Setting up (expensive)..."
|
|
182
|
-
* setup() // Does nothing (cached)
|
|
183
|
-
* setup() // Does nothing (cached)
|
|
184
|
-
* ```
|
|
185
|
-
*/
|
|
186
|
-
export function memoize<TArgs extends any[], TReturn>(
|
|
187
|
-
fn: (...args: TArgs) => TReturn
|
|
188
|
-
): (...args: TArgs) => TReturn {
|
|
189
|
-
const cached: { hasValue: boolean; value: TReturn } = { hasValue: false, value: undefined as any };
|
|
190
|
-
|
|
191
|
-
return (...args: TArgs) => {
|
|
192
|
-
if (!cached.hasValue) {
|
|
193
|
-
cached.value = fn(...args);
|
|
194
|
-
cached.hasValue = true;
|
|
195
|
-
}
|
|
196
|
-
return cached.value;
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Retry - retry a function on failure
|
|
202
|
-
*
|
|
203
|
-
* @example
|
|
204
|
-
* ```typescript
|
|
205
|
-
* const setupWithRetry = retry(
|
|
206
|
-
* async () => {
|
|
207
|
-
* await fetch('https://api.example.com/init')
|
|
208
|
-
* },
|
|
209
|
-
* { maxAttempts: 3, delayMs: 1000 }
|
|
210
|
-
* )
|
|
211
|
-
*
|
|
212
|
-
* await setupWithRetry()
|
|
213
|
-
* ```
|
|
214
|
-
*/
|
|
215
|
-
export function retry<TArgs extends any[], TReturn>(
|
|
216
|
-
fn: (...args: TArgs) => Promise<TReturn>,
|
|
217
|
-
options: {
|
|
218
|
-
maxAttempts?: number;
|
|
219
|
-
delayMs?: number;
|
|
220
|
-
onRetry?: (attempt: number, error: Error) => void;
|
|
221
|
-
} = {}
|
|
222
|
-
): (...args: TArgs) => Promise<TReturn> {
|
|
223
|
-
const { maxAttempts = 3, delayMs = 1000, onRetry } = options;
|
|
224
|
-
|
|
225
|
-
return async (...args: TArgs) => {
|
|
226
|
-
let lastError: Error | undefined;
|
|
227
|
-
|
|
228
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
229
|
-
try {
|
|
230
|
-
return await fn(...args);
|
|
231
|
-
} catch (error) {
|
|
232
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
233
|
-
|
|
234
|
-
if (attempt < maxAttempts) {
|
|
235
|
-
onRetry?.(attempt, lastError);
|
|
236
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
throw lastError;
|
|
242
|
-
};
|
|
243
|
-
}
|