autotel 2.22.0 → 2.23.1
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 +112 -6
- package/bin/intent.js +6 -0
- package/dist/auto.cjs +3 -3
- package/dist/auto.js +2 -2
- package/dist/{chunk-EWH2542B.js → chunk-3AMR5XLZ.js} +3 -3
- package/dist/{chunk-EWH2542B.js.map → chunk-3AMR5XLZ.js.map} +1 -1
- package/dist/chunk-3QXBFGKP.js +344 -0
- package/dist/chunk-3QXBFGKP.js.map +1 -0
- package/dist/{chunk-VQFF2WMP.cjs → chunk-3ZFDJJWZ.cjs} +37 -29
- package/dist/chunk-3ZFDJJWZ.cjs.map +1 -0
- package/dist/{chunk-CQC6RVLR.cjs → chunk-4RZ4JUBY.cjs} +5 -5
- package/dist/{chunk-CQC6RVLR.cjs.map → chunk-4RZ4JUBY.cjs.map} +1 -1
- package/dist/{chunk-PAVYKPCQ.js → chunk-5XUEHX7J.js} +3 -3
- package/dist/{chunk-PAVYKPCQ.js.map → chunk-5XUEHX7J.js.map} +1 -1
- package/dist/chunk-6S5RUKU3.cjs +347 -0
- package/dist/chunk-6S5RUKU3.cjs.map +1 -0
- package/dist/{chunk-BS757SL2.js → chunk-724XLWR3.js} +9 -4
- package/dist/chunk-724XLWR3.js.map +1 -0
- package/dist/chunk-7EQ4G4SI.cjs +146 -0
- package/dist/chunk-7EQ4G4SI.cjs.map +1 -0
- package/dist/{chunk-CQP5SQT4.cjs → chunk-AXFWWJF3.cjs} +7 -7
- package/dist/{chunk-CQP5SQT4.cjs.map → chunk-AXFWWJF3.cjs.map} +1 -1
- package/dist/{chunk-7NH625MS.cjs → chunk-BSZP4URK.cjs} +5 -5
- package/dist/{chunk-7NH625MS.cjs.map → chunk-BSZP4URK.cjs.map} +1 -1
- package/dist/{chunk-GZFH6P5U.js → chunk-GY4CRZSV.js} +14 -6
- package/dist/chunk-GY4CRZSV.js.map +1 -0
- package/dist/{chunk-QKUGUDXJ.cjs → chunk-HSEIUH7F.cjs} +10 -5
- package/dist/chunk-HSEIUH7F.cjs.map +1 -0
- package/dist/{chunk-DTW3WB7Z.js → chunk-IPKXURBW.js} +3 -3
- package/dist/{chunk-DTW3WB7Z.js.map → chunk-IPKXURBW.js.map} +1 -1
- package/dist/chunk-J7VGRIAJ.js +64 -0
- package/dist/chunk-J7VGRIAJ.js.map +1 -0
- package/dist/chunk-KFOHQK7X.js +144 -0
- package/dist/chunk-KFOHQK7X.js.map +1 -0
- package/dist/{chunk-4UYR46UP.cjs → chunk-MSUHW2I4.cjs} +13 -13
- package/dist/{chunk-4UYR46UP.cjs.map → chunk-MSUHW2I4.cjs.map} +1 -1
- package/dist/chunk-T4B5LB6E.cjs +66 -0
- package/dist/chunk-T4B5LB6E.cjs.map +1 -0
- package/dist/{chunk-QHT4MUED.js → chunk-WCIIFRGL.js} +3 -3
- package/dist/{chunk-QHT4MUED.js.map → chunk-WCIIFRGL.js.map} +1 -1
- package/dist/decorators.cjs +3 -3
- package/dist/decorators.js +3 -3
- package/dist/drain-pipeline.cjs +13 -0
- package/dist/drain-pipeline.cjs.map +1 -0
- package/dist/drain-pipeline.d.cts +37 -0
- package/dist/drain-pipeline.d.ts +37 -0
- package/dist/drain-pipeline.js +4 -0
- package/dist/drain-pipeline.js.map +1 -0
- package/dist/enrichers.cjs +90 -0
- package/dist/enrichers.cjs.map +1 -0
- package/dist/enrichers.d.cts +23 -0
- package/dist/enrichers.d.ts +23 -0
- package/dist/enrichers.js +86 -0
- package/dist/enrichers.js.map +1 -0
- package/dist/event.cjs +6 -6
- package/dist/event.js +3 -3
- package/dist/functional.cjs +10 -10
- package/dist/functional.js +3 -3
- package/dist/index.cjs +274 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -3
- package/dist/index.d.ts +73 -3
- package/dist/index.js +227 -11
- package/dist/index.js.map +1 -1
- package/dist/{init-BMiXSJNM.d.cts → init-BC5aN8bh.d.cts} +18 -0
- package/dist/{init-ByRbNTRo.d.ts → init-_FG4IbhF.d.ts} +18 -0
- package/dist/instrumentation.cjs +9 -9
- package/dist/instrumentation.js +2 -2
- package/dist/messaging.cjs +7 -7
- package/dist/messaging.js +4 -4
- package/dist/parse-error.cjs +13 -0
- package/dist/parse-error.cjs.map +1 -0
- package/dist/parse-error.d.cts +13 -0
- package/dist/parse-error.d.ts +13 -0
- package/dist/parse-error.js +4 -0
- package/dist/parse-error.js.map +1 -0
- package/dist/processors.cjs +2 -2
- package/dist/processors.d.cts +40 -4
- package/dist/processors.d.ts +40 -4
- package/dist/processors.js +1 -1
- package/dist/semantic-helpers.cjs +8 -8
- package/dist/semantic-helpers.js +4 -4
- package/dist/webhook.cjs +4 -4
- package/dist/webhook.js +3 -3
- package/dist/workflow-distributed.cjs +5 -5
- package/dist/workflow-distributed.js +3 -3
- package/dist/workflow.cjs +8 -8
- package/dist/workflow.js +4 -4
- package/dist/yaml-config.d.cts +2 -1
- package/dist/yaml-config.d.ts +2 -1
- package/package.json +55 -33
- package/skills/autotel-core/SKILL.md +162 -0
- package/skills/autotel-events/SKILL.md +88 -0
- package/skills/autotel-frameworks/SKILL.md +97 -0
- package/skills/autotel-instrumentation/SKILL.md +125 -0
- package/skills/autotel-request-logging/SKILL.md +100 -0
- package/skills/autotel-structured-errors/SKILL.md +130 -0
- package/src/drain-pipeline.test.ts +68 -0
- package/src/drain-pipeline.ts +199 -0
- package/src/enrichers.test.ts +150 -0
- package/src/enrichers.ts +145 -0
- package/src/flatten-attributes.test.ts +76 -0
- package/src/flatten-attributes.ts +80 -0
- package/src/functional.test.ts +63 -0
- package/src/functional.ts +11 -3
- package/src/index.ts +34 -0
- package/src/init.ts +22 -0
- package/src/package-manifest.test.ts +24 -0
- package/src/parse-error.test.ts +73 -0
- package/src/parse-error.ts +112 -0
- package/src/pretty-log-formatter.test.ts +123 -0
- package/src/pretty-log-formatter.ts +210 -0
- package/src/processors/canonical-log-line-processor.test.ts +81 -25
- package/src/processors/canonical-log-line-processor.ts +130 -42
- package/src/request-logger.test.ts +137 -0
- package/src/request-logger.ts +150 -0
- package/src/structured-error.test.ts +106 -0
- package/src/structured-error.ts +97 -0
- package/dist/chunk-2RQDNGV3.js +0 -126
- package/dist/chunk-2RQDNGV3.js.map +0 -1
- package/dist/chunk-BS757SL2.js.map +0 -1
- package/dist/chunk-GZFH6P5U.js.map +0 -1
- package/dist/chunk-ONK2Y22L.cjs +0 -128
- package/dist/chunk-ONK2Y22L.cjs.map +0 -1
- package/dist/chunk-QKUGUDXJ.cjs.map +0 -1
- package/dist/chunk-VQFF2WMP.cjs.map +0 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autotel-instrumentation
|
|
3
|
+
description: >
|
|
4
|
+
trace(), span(), instrument(), init(). Factory vs direct pattern, name inference. Sync init; use node-require for optional deps. Load when wrapping handlers or functions with spans.
|
|
5
|
+
type: core
|
|
6
|
+
library: autotel
|
|
7
|
+
library_version: '2.23.0'
|
|
8
|
+
sources:
|
|
9
|
+
- jagreehal/autotel:docs/ARCHITECTURE.md
|
|
10
|
+
- jagreehal/autotel:packages/autotel/src/functional.ts
|
|
11
|
+
- jagreehal/autotel:packages/autotel/CLAUDE.md
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Autotel — Instrumentation
|
|
15
|
+
|
|
16
|
+
Wrap functions and handlers with `trace()`, `span()`, or `instrument()`. Call `init()` once at app startup. Keep init synchronous; use `safeRequire`/`requireModule` for optional dependencies.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { init, trace } from 'autotel';
|
|
22
|
+
|
|
23
|
+
init({ service: 'my-app' });
|
|
24
|
+
|
|
25
|
+
const handler = trace(async (req: Request) => {
|
|
26
|
+
return processRequest(req);
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
With span context (attributes, request logger):
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const handler = trace((ctx) => async (req: Request) => {
|
|
34
|
+
ctx.setAttribute('http.route', '/api/checkout');
|
|
35
|
+
const log = getRequestLogger(ctx);
|
|
36
|
+
log.set({ path: req.url });
|
|
37
|
+
return processRequest(req);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Core Patterns
|
|
42
|
+
|
|
43
|
+
**Explicit span name:**
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const checkout = trace('checkout', async (body) => {
|
|
47
|
+
return processCheckout(body);
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**instrument() with key:**
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { instrument } from 'autotel';
|
|
55
|
+
const fn = instrument({
|
|
56
|
+
key: 'processOrder',
|
|
57
|
+
fn: async (id) => db.orders.get(id),
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**span() for a child span:**
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { span } from 'autotel';
|
|
65
|
+
const result = await span('db.query', async () => db.query(sql));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Init with optional config:**
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
init({
|
|
72
|
+
service: 'my-api',
|
|
73
|
+
// see docs/CONFIGURATION.md for full options
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Common Mistakes
|
|
78
|
+
|
|
79
|
+
### HIGH Forget to call init() before using trace/span
|
|
80
|
+
|
|
81
|
+
Wrong:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { trace } from 'autotel';
|
|
85
|
+
export const fn = trace(async () => {});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Correct:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { init, trace } from 'autotel';
|
|
92
|
+
init({ service: 'my-app' });
|
|
93
|
+
export const fn = trace(async () => {});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
SDK must be initialized; trace() and span() rely on the global tracer.
|
|
97
|
+
|
|
98
|
+
Source: packages/autotel/CLAUDE.md
|
|
99
|
+
|
|
100
|
+
### MEDIUM Wrong export path for submodules
|
|
101
|
+
|
|
102
|
+
Wrong:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { Event } from 'autotel'; // Event is on autotel/event
|
|
106
|
+
import { createTraceCollector } from 'autotel'; // testing subpath
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Correct:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { trace, init, getRequestLogger } from 'autotel';
|
|
113
|
+
import { Event } from 'autotel/event';
|
|
114
|
+
import { createTraceCollector } from 'autotel/testing';
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use the exact export paths from package.json (autotel/event, autotel/testing, autotel/attributes, etc.).
|
|
118
|
+
|
|
119
|
+
Source: packages/autotel/package.json exports
|
|
120
|
+
|
|
121
|
+
## Version
|
|
122
|
+
|
|
123
|
+
Targets autotel v2.23.x.
|
|
124
|
+
|
|
125
|
+
See also: autotel-core/SKILL.md — when to use what. autotel-request-logging/SKILL.md — getRequestLogger requires active span.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autotel-request-logging
|
|
3
|
+
description: >
|
|
4
|
+
getRequestLogger(), set(), info/warn/error, emitNow(). One snapshot per request; requires active span. Use when adding request-scoped context or replacing scattered console.log.
|
|
5
|
+
type: core
|
|
6
|
+
library: autotel
|
|
7
|
+
library_version: '2.23.0'
|
|
8
|
+
requires:
|
|
9
|
+
- autotel-instrumentation
|
|
10
|
+
sources:
|
|
11
|
+
- jagreehal/autotel:packages/autotel/src/request-logger.ts
|
|
12
|
+
- jagreehal/autotel:docs/AGENT-GUIDE.md
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Autotel — Request Logging
|
|
16
|
+
|
|
17
|
+
This skill builds on autotel-instrumentation. Read it first for init and span creation.
|
|
18
|
+
|
|
19
|
+
Accumulate context with `getRequestLogger(ctx)`, `.set()`, and `.info()`/`.warn()`/`.error()`. Call `.emitNow()` (or rely on middleware) to emit one snapshot per request. Request logger requires an active span — use inside `trace()` or framework middleware.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { init, trace, getRequestLogger } from 'autotel';
|
|
25
|
+
|
|
26
|
+
init({ service: 'my-app' });
|
|
27
|
+
|
|
28
|
+
const handler = trace((ctx) => async (req: Request, res: Response) => {
|
|
29
|
+
const log = getRequestLogger(ctx);
|
|
30
|
+
log.set({ method: req.method, path: req.url });
|
|
31
|
+
|
|
32
|
+
const user = await getAuth(req);
|
|
33
|
+
log.set({ user: { id: user.id } });
|
|
34
|
+
|
|
35
|
+
const result = await doWork(req);
|
|
36
|
+
log.set({ result: { id: result.id } });
|
|
37
|
+
log.emitNow();
|
|
38
|
+
return res.json(result);
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
When the framework creates the span (e.g. Hono middleware), call `getRequestLogger()` with no args:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
app.use(autotelMiddleware());
|
|
46
|
+
app.get('/api/users', (c) => {
|
|
47
|
+
const log = getRequestLogger();
|
|
48
|
+
log.set({ route: 'users' });
|
|
49
|
+
return c.json({ users: [] });
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Core Patterns
|
|
54
|
+
|
|
55
|
+
**Accumulate then emit:**
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const log = getRequestLogger(ctx);
|
|
59
|
+
log.set({ cart: { items: body.items.length } });
|
|
60
|
+
log.set({ payment: { method: body.method } });
|
|
61
|
+
log.error(err, { step: 'payment' });
|
|
62
|
+
log.emitNow();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**RequestLogSnapshot:** `emitNow()` returns `{ timestamp, traceId, spanId, correlationId, context }`. You can pass `onEmit` in options to forward it.
|
|
66
|
+
|
|
67
|
+
**Options:** `getRequestLogger(ctx?, { onEmit?: (snapshot) => void })` for custom fan-out on emit.
|
|
68
|
+
|
|
69
|
+
## Common Mistakes
|
|
70
|
+
|
|
71
|
+
### HIGH Call getRequestLogger() outside a span
|
|
72
|
+
|
|
73
|
+
Wrong:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
function helper() {
|
|
77
|
+
const log = getRequestLogger();
|
|
78
|
+
log.set({ step: 'helper' });
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Correct:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Ensure helper runs inside trace() or middleware-created span
|
|
86
|
+
function helper(ctx?: TraceContext) {
|
|
87
|
+
const log = getRequestLogger(ctx);
|
|
88
|
+
log.set({ step: 'helper' });
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Request logger attaches to the active span. If there is no active span, it throws. Wrap the call path with `trace()` or register middleware that creates a span per request.
|
|
93
|
+
|
|
94
|
+
Source: packages/autotel/src/request-logger.ts
|
|
95
|
+
|
|
96
|
+
## Version
|
|
97
|
+
|
|
98
|
+
Targets autotel v2.23.x.
|
|
99
|
+
|
|
100
|
+
See also: autotel-instrumentation/SKILL.md — creating the span. autotel-structured-errors/SKILL.md — use .error() to record errors in the snapshot.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autotel-structured-errors
|
|
3
|
+
description: >
|
|
4
|
+
createStructuredError, parseError, recordStructuredError. API errors with message, why, fix, link; client parsing for UI. Use in API routes and client catch blocks.
|
|
5
|
+
type: core
|
|
6
|
+
library: autotel
|
|
7
|
+
library_version: '2.23.0'
|
|
8
|
+
sources:
|
|
9
|
+
- jagreehal/autotel:packages/autotel/src/structured-error.ts
|
|
10
|
+
- jagreehal/autotel:packages/autotel/src/parse-error.ts
|
|
11
|
+
- jagreehal/autotel:docs/AGENT-GUIDE.md
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Autotel — Structured Errors
|
|
15
|
+
|
|
16
|
+
Throw errors with `createStructuredError({ message, why?, fix?, link?, status?, cause? })` in API routes and services. On the client, use `parseError(caught)` to get `message`, `status`, `why`, `fix`, `link` for toasts and UI.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
**Server (API route or service):**
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createStructuredError } from 'autotel';
|
|
24
|
+
|
|
25
|
+
if (!user) {
|
|
26
|
+
throw createStructuredError({
|
|
27
|
+
message: 'User not found',
|
|
28
|
+
status: 404,
|
|
29
|
+
why: `No user with ID "${userId}"`,
|
|
30
|
+
fix: 'Check the user ID and try again',
|
|
31
|
+
link: 'https://docs.example.com/errors/user-not-found',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Client:**
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { parseError } from 'autotel';
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await fetch('/api/checkout', { method: 'POST', body: JSON.stringify(data) });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const e = parseError(err);
|
|
45
|
+
toast.error(e.message, { description: e.why });
|
|
46
|
+
if (e.fix) setHelp(e.fix);
|
|
47
|
+
if (e.link) setDocLink(e.link);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Core Patterns
|
|
52
|
+
|
|
53
|
+
**Wrap a caught error (preserve cause):**
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
try {
|
|
57
|
+
await stripe.charges.create(data);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw createStructuredError({
|
|
60
|
+
message: 'Payment failed',
|
|
61
|
+
status: 402,
|
|
62
|
+
why: err instanceof Error ? err.message : 'Unknown error',
|
|
63
|
+
fix: 'Try a different payment method or contact support',
|
|
64
|
+
link: 'https://docs.stripe.com/declines',
|
|
65
|
+
cause: err,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Record on current span:** Use `recordStructuredError(ctx, error)` or the request logger's `.error(error, fields)` so the span gets error attributes and status.
|
|
71
|
+
|
|
72
|
+
**parseError** handles FetchError (ofetch), nested `data.data`, and plain Error. Returns `{ message, status, why?, fix?, link?, raw }`.
|
|
73
|
+
|
|
74
|
+
## Common Mistakes
|
|
75
|
+
|
|
76
|
+
### HIGH Throw new Error() in API routes instead of createStructuredError
|
|
77
|
+
|
|
78
|
+
Wrong:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
throw new Error('Payment failed');
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Correct:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
throw createStructuredError({
|
|
88
|
+
message: 'Payment failed',
|
|
89
|
+
status: 402,
|
|
90
|
+
why: 'Card declined by issuer',
|
|
91
|
+
fix: 'Try a different payment method',
|
|
92
|
+
link: 'https://docs.example.com/payments',
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Clients and agents need structured fields (why, fix, link) for actionable errors. parseError() reads these from API responses.
|
|
97
|
+
|
|
98
|
+
Source: docs/AGENT-GUIDE.md, AGENTS.md
|
|
99
|
+
|
|
100
|
+
### MEDIUM Client only shows error.message and ignores why/fix/link
|
|
101
|
+
|
|
102
|
+
Wrong:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
catch (err) {
|
|
106
|
+
toast.error(err.message);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Correct:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { parseError } from 'autotel';
|
|
114
|
+
catch (err) {
|
|
115
|
+
const e = parseError(err);
|
|
116
|
+
toast.error(e.message, { description: e.why });
|
|
117
|
+
if (e.fix) showFix(e.fix);
|
|
118
|
+
if (e.link) setDocLink(e.link);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
parseError() extracts status, why, fix, and link from API error responses and FetchError so the UI can show them.
|
|
123
|
+
|
|
124
|
+
Source: docs/AGENT-GUIDE.md
|
|
125
|
+
|
|
126
|
+
## Version
|
|
127
|
+
|
|
128
|
+
Targets autotel v2.23.x.
|
|
129
|
+
|
|
130
|
+
See also: autotel-request-logging/SKILL.md — use .error() to record errors in the request snapshot.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createDrainPipeline } from './drain-pipeline';
|
|
3
|
+
|
|
4
|
+
describe('createDrainPipeline', () => {
|
|
5
|
+
it('batches by size and sends to drain', async () => {
|
|
6
|
+
const batchDrain = vi.fn(async () => {});
|
|
7
|
+
const pipeline = createDrainPipeline<number>({
|
|
8
|
+
batch: { size: 2, intervalMs: 1000 },
|
|
9
|
+
});
|
|
10
|
+
const drain = pipeline(batchDrain);
|
|
11
|
+
|
|
12
|
+
drain(1);
|
|
13
|
+
drain(2);
|
|
14
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
15
|
+
|
|
16
|
+
expect(batchDrain).toHaveBeenCalledTimes(1);
|
|
17
|
+
expect(batchDrain).toHaveBeenCalledWith([1, 2]);
|
|
18
|
+
expect(drain.pending).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('retries failed batches and eventually succeeds', async () => {
|
|
22
|
+
let attempts = 0;
|
|
23
|
+
const batchDrain = vi.fn(async () => {
|
|
24
|
+
attempts++;
|
|
25
|
+
if (attempts < 2) throw new Error('temporary');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const pipeline = createDrainPipeline<number>({
|
|
29
|
+
batch: { size: 1, intervalMs: 1000 },
|
|
30
|
+
retry: {
|
|
31
|
+
maxAttempts: 3,
|
|
32
|
+
initialDelayMs: 1,
|
|
33
|
+
maxDelayMs: 2,
|
|
34
|
+
backoff: 'fixed',
|
|
35
|
+
jitter: false,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const drain = pipeline(batchDrain);
|
|
39
|
+
|
|
40
|
+
drain(42);
|
|
41
|
+
await drain.flush();
|
|
42
|
+
|
|
43
|
+
expect(batchDrain).toHaveBeenCalledTimes(2);
|
|
44
|
+
expect(drain.pending).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('drops overflowed events based on policy', async () => {
|
|
48
|
+
const dropped: number[] = [];
|
|
49
|
+
const batchDrain = vi.fn(async () => {});
|
|
50
|
+
const pipeline = createDrainPipeline<number>({
|
|
51
|
+
batch: { size: 10, intervalMs: 1000 },
|
|
52
|
+
maxBufferSize: 2,
|
|
53
|
+
dropPolicy: 'oldest',
|
|
54
|
+
onDropped: (events) => dropped.push(...events),
|
|
55
|
+
});
|
|
56
|
+
const drain = pipeline(batchDrain);
|
|
57
|
+
|
|
58
|
+
drain(1);
|
|
59
|
+
drain(2);
|
|
60
|
+
drain(3); // drops 1
|
|
61
|
+
|
|
62
|
+
expect(dropped).toEqual([1]);
|
|
63
|
+
expect(drain.pending).toBe(2);
|
|
64
|
+
|
|
65
|
+
await drain.flush();
|
|
66
|
+
expect(batchDrain).toHaveBeenCalledWith([2, 3]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
export interface DrainPipelineOptions<T = unknown> {
|
|
2
|
+
batch?: {
|
|
3
|
+
/** Maximum events per batch. @default 50 */
|
|
4
|
+
size?: number;
|
|
5
|
+
/** Max time an event can stay buffered before flush. @default 5000 */
|
|
6
|
+
intervalMs?: number;
|
|
7
|
+
};
|
|
8
|
+
retry?: {
|
|
9
|
+
/** Total attempts including first try. @default 3 */
|
|
10
|
+
maxAttempts?: number;
|
|
11
|
+
/** Delay strategy between attempts. @default 'exponential' */
|
|
12
|
+
backoff?: 'exponential' | 'linear' | 'fixed';
|
|
13
|
+
/** Base delay for first retry. @default 1000 */
|
|
14
|
+
initialDelayMs?: number;
|
|
15
|
+
/** Max delay cap. @default 30000 */
|
|
16
|
+
maxDelayMs?: number;
|
|
17
|
+
/** Add random jitter to delays. @default true */
|
|
18
|
+
jitter?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/** Max buffered events before dropping. @default 1000 */
|
|
21
|
+
maxBufferSize?: number;
|
|
22
|
+
/** Overflow policy. @default 'oldest' */
|
|
23
|
+
dropPolicy?: 'oldest' | 'newest';
|
|
24
|
+
/** Called when events are dropped from overflow or exhausted retries. */
|
|
25
|
+
onDropped?: (events: T[], error?: Error) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PipelineDrainFn<T> {
|
|
29
|
+
(ctx: T): void;
|
|
30
|
+
/** Flush all buffered events. */
|
|
31
|
+
flush: () => Promise<void>;
|
|
32
|
+
/** Flush and stop scheduling future timer work. */
|
|
33
|
+
shutdown: () => Promise<void>;
|
|
34
|
+
readonly pending: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function wait(ms: number): Promise<void> {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const timer = setTimeout(resolve, ms);
|
|
40
|
+
timer.unref?.();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createDrainPipeline<T = unknown>(
|
|
45
|
+
options?: DrainPipelineOptions<T>,
|
|
46
|
+
): (drain: (batch: T[]) => void | Promise<void>) => PipelineDrainFn<T> {
|
|
47
|
+
const batchSize = options?.batch?.size ?? 50;
|
|
48
|
+
const intervalMs = options?.batch?.intervalMs ?? 5000;
|
|
49
|
+
const maxBufferSize = options?.maxBufferSize ?? 1000;
|
|
50
|
+
const maxAttempts = options?.retry?.maxAttempts ?? 3;
|
|
51
|
+
const backoff = options?.retry?.backoff ?? 'exponential';
|
|
52
|
+
const initialDelayMs = options?.retry?.initialDelayMs ?? 1000;
|
|
53
|
+
const maxDelayMs = options?.retry?.maxDelayMs ?? 30_000;
|
|
54
|
+
const jitter = options?.retry?.jitter ?? true;
|
|
55
|
+
const dropPolicy = options?.dropPolicy ?? 'oldest';
|
|
56
|
+
const onDropped = options?.onDropped;
|
|
57
|
+
|
|
58
|
+
if (!Number.isFinite(batchSize) || batchSize <= 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`[autotel/drain-pipeline] batch.size must be a positive finite number, got: ${batchSize}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`[autotel/drain-pipeline] batch.intervalMs must be a positive finite number, got: ${intervalMs}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!Number.isFinite(maxBufferSize) || maxBufferSize <= 0) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`[autotel/drain-pipeline] maxBufferSize must be a positive finite number, got: ${maxBufferSize}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (!Number.isFinite(maxAttempts) || maxAttempts <= 0) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`[autotel/drain-pipeline] retry.maxAttempts must be a positive finite number, got: ${maxAttempts}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (drain: (batch: T[]) => void | Promise<void>): PipelineDrainFn<T> => {
|
|
80
|
+
const buffer: T[] = [];
|
|
81
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
82
|
+
let activeFlush: Promise<void> | null = null;
|
|
83
|
+
let isShutdown = false;
|
|
84
|
+
|
|
85
|
+
const clearTimer = () => {
|
|
86
|
+
if (timer) {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
timer = null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const computeDelay = (attempt: number): number => {
|
|
93
|
+
const base =
|
|
94
|
+
backoff === 'fixed'
|
|
95
|
+
? initialDelayMs
|
|
96
|
+
: backoff === 'linear'
|
|
97
|
+
? initialDelayMs * attempt
|
|
98
|
+
: initialDelayMs * 2 ** (attempt - 1);
|
|
99
|
+
|
|
100
|
+
const bounded = Math.min(base, maxDelayMs);
|
|
101
|
+
if (!jitter || bounded <= 0) return bounded;
|
|
102
|
+
const factor = 0.5 + Math.random(); // [0.5, 1.5)
|
|
103
|
+
return Math.max(0, Math.round(bounded * factor));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const sendWithRetry = async (batch: T[]): Promise<void> => {
|
|
107
|
+
let lastError: Error | undefined;
|
|
108
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
109
|
+
try {
|
|
110
|
+
await drain(batch);
|
|
111
|
+
return;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
114
|
+
if (attempt < maxAttempts) {
|
|
115
|
+
await wait(computeDelay(attempt));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
onDropped?.(batch, lastError);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const drainBuffer = async (): Promise<void> => {
|
|
123
|
+
while (buffer.length > 0) {
|
|
124
|
+
const batch = buffer.splice(0, batchSize);
|
|
125
|
+
await sendWithRetry(batch);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const scheduleFlush = () => {
|
|
130
|
+
if (isShutdown || timer || activeFlush) return;
|
|
131
|
+
timer = setTimeout(() => {
|
|
132
|
+
timer = null;
|
|
133
|
+
startFlush();
|
|
134
|
+
}, intervalMs);
|
|
135
|
+
timer.unref?.();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const startFlush = () => {
|
|
139
|
+
if (activeFlush || isShutdown) return;
|
|
140
|
+
activeFlush = drainBuffer().finally(() => {
|
|
141
|
+
activeFlush = null;
|
|
142
|
+
if (isShutdown) return;
|
|
143
|
+
if (buffer.length >= batchSize) {
|
|
144
|
+
startFlush();
|
|
145
|
+
} else if (buffer.length > 0) {
|
|
146
|
+
scheduleFlush();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const push = (ctx: T) => {
|
|
152
|
+
if (isShutdown) return;
|
|
153
|
+
|
|
154
|
+
if (buffer.length >= maxBufferSize) {
|
|
155
|
+
if (dropPolicy === 'newest') {
|
|
156
|
+
onDropped?.([ctx]);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const dropped = buffer.splice(0, 1);
|
|
160
|
+
onDropped?.(dropped);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
buffer.push(ctx);
|
|
164
|
+
if (buffer.length >= batchSize) {
|
|
165
|
+
clearTimer();
|
|
166
|
+
startFlush();
|
|
167
|
+
} else {
|
|
168
|
+
scheduleFlush();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const flush = async (): Promise<void> => {
|
|
173
|
+
clearTimer();
|
|
174
|
+
if (activeFlush) await activeFlush;
|
|
175
|
+
|
|
176
|
+
const snapshot = buffer.length;
|
|
177
|
+
if (snapshot <= 0) return;
|
|
178
|
+
const toFlush = buffer.splice(0, snapshot);
|
|
179
|
+
while (toFlush.length > 0) {
|
|
180
|
+
const batch = toFlush.splice(0, batchSize);
|
|
181
|
+
await sendWithRetry(batch);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const shutdown = async (): Promise<void> => {
|
|
186
|
+
isShutdown = true;
|
|
187
|
+
await flush();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const fn = push as PipelineDrainFn<T>;
|
|
191
|
+
fn.flush = flush;
|
|
192
|
+
fn.shutdown = shutdown;
|
|
193
|
+
Object.defineProperty(fn, 'pending', {
|
|
194
|
+
enumerable: true,
|
|
195
|
+
get: () => buffer.length,
|
|
196
|
+
});
|
|
197
|
+
return fn;
|
|
198
|
+
};
|
|
199
|
+
}
|