autotel 2.23.0 → 2.24.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.
- package/bin/intent.js +6 -0
- package/dist/auto.cjs +2 -2
- package/dist/auto.js +1 -1
- package/dist/{chunk-GY4CRZSV.js → chunk-2D74YR4Z.js} +3 -3
- package/dist/{chunk-GY4CRZSV.js.map → chunk-2D74YR4Z.js.map} +1 -1
- package/dist/{chunk-724XLWR3.js → chunk-47KW5CV7.js} +87 -4
- package/dist/chunk-47KW5CV7.js.map +1 -0
- package/dist/{chunk-3ZFDJJWZ.cjs → chunk-AXNPD6XW.cjs} +26 -26
- package/dist/{chunk-3ZFDJJWZ.cjs.map → chunk-AXNPD6XW.cjs.map} +1 -1
- package/dist/{chunk-BSZP4URK.cjs → chunk-CLR5RQW4.cjs} +5 -5
- package/dist/{chunk-BSZP4URK.cjs.map → chunk-CLR5RQW4.cjs.map} +1 -1
- package/dist/{chunk-AXFWWJF3.cjs → chunk-CSG2D3BZ.cjs} +7 -7
- package/dist/{chunk-AXFWWJF3.cjs.map → chunk-CSG2D3BZ.cjs.map} +1 -1
- package/dist/{chunk-MSUHW2I4.cjs → chunk-CZR7PJUZ.cjs} +13 -13
- package/dist/{chunk-MSUHW2I4.cjs.map → chunk-CZR7PJUZ.cjs.map} +1 -1
- package/dist/{chunk-IPKXURBW.js → chunk-GSZJOYFF.js} +3 -3
- package/dist/{chunk-IPKXURBW.js.map → chunk-GSZJOYFF.js.map} +1 -1
- package/dist/{chunk-WCIIFRGL.js → chunk-IJMW6CTV.js} +3 -3
- package/dist/{chunk-WCIIFRGL.js.map → chunk-IJMW6CTV.js.map} +1 -1
- package/dist/{chunk-HSEIUH7F.cjs → chunk-PDRIJURL.cjs} +86 -2
- package/dist/chunk-PDRIJURL.cjs.map +1 -0
- package/dist/{chunk-4RZ4JUBY.cjs → chunk-W6U2BUHU.cjs} +5 -5
- package/dist/{chunk-4RZ4JUBY.cjs.map → chunk-W6U2BUHU.cjs.map} +1 -1
- package/dist/{chunk-5XUEHX7J.js → chunk-Z3BQYTZE.js} +3 -3
- package/dist/{chunk-5XUEHX7J.js.map → chunk-Z3BQYTZE.js.map} +1 -1
- package/dist/{chunk-3AMR5XLZ.js → chunk-ZJTBAUXG.js} +3 -3
- package/dist/{chunk-3AMR5XLZ.js.map → chunk-ZJTBAUXG.js.map} +1 -1
- package/dist/decorators.cjs +2 -2
- package/dist/decorators.js +2 -2
- 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 +5 -5
- package/dist/event.js +2 -2
- package/dist/functional.cjs +9 -9
- package/dist/functional.js +2 -2
- package/dist/index.cjs +63 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +28 -11
- package/dist/index.js.map +1 -1
- package/dist/{init-BC5aN8bh.d.cts → init-Co9CWOY6.d.cts} +16 -0
- package/dist/{init-_FG4IbhF.d.ts → init-oLqNJTpj.d.ts} +16 -0
- package/dist/instrumentation.cjs +8 -8
- package/dist/instrumentation.js +1 -1
- package/dist/messaging.cjs +6 -6
- package/dist/messaging.js +3 -3
- package/dist/semantic-helpers.cjs +7 -7
- package/dist/semantic-helpers.js +3 -3
- package/dist/webhook.cjs +3 -3
- package/dist/webhook.js +2 -2
- package/dist/workflow-distributed.cjs +4 -4
- package/dist/workflow-distributed.js +2 -2
- package/dist/workflow.cjs +7 -7
- package/dist/workflow.js +3 -3
- package/dist/yaml-config.d.cts +1 -1
- package/dist/yaml-config.d.ts +1 -1
- package/package.json +45 -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/enrichers.test.ts +150 -0
- package/src/enrichers.ts +145 -0
- package/src/index.ts +4 -0
- package/src/init-redactor.test.ts +8 -0
- package/src/init.ts +56 -0
- package/src/package-manifest.test.ts +24 -0
- package/src/posthog-logs.test.ts +115 -0
- package/src/posthog-logs.ts +77 -0
- package/src/redact-values.test.ts +76 -0
- package/src/redact-values.ts +27 -0
- package/src/request-logger.test.ts +14 -1
- package/src/request-logger.ts +11 -1
- package/src/structured-error.test.ts +30 -0
- package/src/structured-error.ts +11 -0
- package/dist/chunk-724XLWR3.js.map +0 -1
- package/dist/chunk-HSEIUH7F.cjs.map +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autotel-frameworks
|
|
3
|
+
description: >
|
|
4
|
+
Hono, Fastify, TanStack Start, Cloudflare Workers. Middleware and init; getRequestLogger() in handlers. Load when adding Autotel to a web framework.
|
|
5
|
+
type: framework
|
|
6
|
+
library: autotel
|
|
7
|
+
library_version: '2.23.0'
|
|
8
|
+
requires:
|
|
9
|
+
- autotel-instrumentation
|
|
10
|
+
sources:
|
|
11
|
+
- jagreehal/autotel:packages/autotel-hono/src/index.ts
|
|
12
|
+
- jagreehal/autotel:docs/AGENT-GUIDE.md
|
|
13
|
+
- jagreehal/autotel:AGENTS.md
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Autotel — Framework Integration
|
|
17
|
+
|
|
18
|
+
This skill builds on autotel-instrumentation. Read it first for init() and span creation.
|
|
19
|
+
|
|
20
|
+
Use framework-specific middleware or wrappers to create a span per request; then call `getRequestLogger()` inside handlers. Each framework package (autotel-hono, autotel-tanstack, autotel-cloudflare) provides the glue.
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
### Hono
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { Hono } from 'hono';
|
|
28
|
+
import { init, getRequestLogger } from 'autotel';
|
|
29
|
+
import { otel } from 'autotel-hono';
|
|
30
|
+
|
|
31
|
+
init({ service: 'my-api' });
|
|
32
|
+
|
|
33
|
+
const app = new Hono();
|
|
34
|
+
app.use('*', otel({ serviceName: 'my-api' }));
|
|
35
|
+
|
|
36
|
+
app.post('/api/checkout', async (c) => {
|
|
37
|
+
const log = getRequestLogger();
|
|
38
|
+
log.set({ route: 'checkout' });
|
|
39
|
+
const body = await c.req.json();
|
|
40
|
+
log.set({ cart: { items: body.items?.length } });
|
|
41
|
+
log.emitNow();
|
|
42
|
+
return c.json({ ok: true });
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Fastify
|
|
47
|
+
|
|
48
|
+
Use middleware or a wrapper that creates a span per request (see apps/example-fastify). Inside the route, wrap with `trace()` or ensure a span is active, then `getRequestLogger(ctx)` or `getRequestLogger()`.
|
|
49
|
+
|
|
50
|
+
### TanStack Start
|
|
51
|
+
|
|
52
|
+
See packages/autotel-tanstack and apps/example-tanstack-start: middleware and env config. Use `getRequestLogger()` inside server handlers when a span is active.
|
|
53
|
+
|
|
54
|
+
### Cloudflare Workers
|
|
55
|
+
|
|
56
|
+
See packages/autotel-cloudflare: init at top level, wrap the fetch handler so each request gets a span. Use `getRequestLogger()` or trace context inside the handler.
|
|
57
|
+
|
|
58
|
+
## Core Patterns
|
|
59
|
+
|
|
60
|
+
**Register middleware before routes:** Middleware creates the span; getRequestLogger() needs that active span.
|
|
61
|
+
|
|
62
|
+
**Framework-specific packages:** autotel-hono, autotel-tanstack, autotel-cloudflare. Install the package for your framework and follow its README or CLAUDE.md.
|
|
63
|
+
|
|
64
|
+
## Common Mistakes
|
|
65
|
+
|
|
66
|
+
### HIGH Use getRequestLogger() in Hono without registering middleware first
|
|
67
|
+
|
|
68
|
+
Wrong:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const app = new Hono();
|
|
72
|
+
app.get('/api/x', (c) => {
|
|
73
|
+
const log = getRequestLogger();
|
|
74
|
+
return c.json({});
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Correct:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const app = new Hono();
|
|
82
|
+
app.use('*', otel());
|
|
83
|
+
app.get('/api/x', (c) => {
|
|
84
|
+
const log = getRequestLogger();
|
|
85
|
+
return c.json({});
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Middleware creates the span per request. Without it, getRequestLogger() has no active span and will throw.
|
|
90
|
+
|
|
91
|
+
Source: packages/autotel-hono, docs/AGENT-GUIDE.md
|
|
92
|
+
|
|
93
|
+
## Version
|
|
94
|
+
|
|
95
|
+
Targets autotel v2.23.x.
|
|
96
|
+
|
|
97
|
+
See also: autotel-instrumentation/SKILL.md — init and spans. autotel-request-logging/SKILL.md — request logger usage.
|
|
@@ -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,150 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { userAgent, geo, requestSize } from './enrichers';
|
|
3
|
+
|
|
4
|
+
describe('enrichers', () => {
|
|
5
|
+
describe('userAgent', () => {
|
|
6
|
+
it('parses Chrome on macOS', () => {
|
|
7
|
+
const result = userAgent({
|
|
8
|
+
'user-agent':
|
|
9
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(result).toMatchObject({
|
|
13
|
+
'user_agent.browser': 'Chrome 120.0.0.0',
|
|
14
|
+
'user_agent.os': 'macOS 10.15.7',
|
|
15
|
+
'user_agent.device': 'desktop',
|
|
16
|
+
});
|
|
17
|
+
expect(result?.['user_agent.raw']).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('parses Firefox on Windows', () => {
|
|
21
|
+
const result = userAgent({
|
|
22
|
+
'user-agent':
|
|
23
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result).toMatchObject({
|
|
27
|
+
'user_agent.browser': 'Firefox 121.0',
|
|
28
|
+
'user_agent.os': 'Windows 10.0',
|
|
29
|
+
'user_agent.device': 'desktop',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('detects mobile device', () => {
|
|
34
|
+
const result = userAgent({
|
|
35
|
+
'user-agent':
|
|
36
|
+
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(result?.['user_agent.device']).toBe('mobile');
|
|
40
|
+
expect(result?.['user_agent.os']).toBe('iOS 17.2');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('detects bot', () => {
|
|
44
|
+
const result = userAgent({
|
|
45
|
+
'user-agent': 'Googlebot/2.1 (+http://www.google.com/bot.html)',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(result?.['user_agent.device']).toBe('bot');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns undefined when no user-agent header', () => {
|
|
52
|
+
expect(userAgent({})).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('accepts mixed-case User-Agent header names', () => {
|
|
56
|
+
const result = userAgent({
|
|
57
|
+
'User-Agent':
|
|
58
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result).toMatchObject({
|
|
62
|
+
'user_agent.browser': 'Firefox 121.0',
|
|
63
|
+
'user_agent.os': 'Windows 10.0',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('geo', () => {
|
|
69
|
+
it('extracts Vercel geo headers', () => {
|
|
70
|
+
const result = geo({
|
|
71
|
+
'x-vercel-ip-country': 'US',
|
|
72
|
+
'x-vercel-ip-country-region': 'CA',
|
|
73
|
+
'x-vercel-ip-city': 'San%20Francisco',
|
|
74
|
+
'x-vercel-ip-latitude': '37.7749',
|
|
75
|
+
'x-vercel-ip-longitude': '-122.4194',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result).toEqual({
|
|
79
|
+
'geo.country': 'US',
|
|
80
|
+
'geo.region': 'CA',
|
|
81
|
+
'geo.city': 'San Francisco',
|
|
82
|
+
'geo.latitude': '37.7749',
|
|
83
|
+
'geo.longitude': '-122.4194',
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('extracts Cloudflare country header', () => {
|
|
88
|
+
const result = geo({ 'cf-ipcountry': 'GB' });
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual({ 'geo.country': 'GB' });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('does not throw on malformed encoded city values', () => {
|
|
94
|
+
expect(() =>
|
|
95
|
+
geo({
|
|
96
|
+
'x-vercel-ip-country': 'US',
|
|
97
|
+
'x-vercel-ip-city': '%E0%A4%A',
|
|
98
|
+
}),
|
|
99
|
+
).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns undefined when no geo headers', () => {
|
|
103
|
+
expect(geo({})).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns longitude when it is the only geo signal', () => {
|
|
107
|
+
const result = geo({ 'x-vercel-ip-longitude': '-0.1276' });
|
|
108
|
+
|
|
109
|
+
expect(result).toEqual({ 'geo.longitude': '-0.1276' });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('requestSize', () => {
|
|
114
|
+
it('extracts request and response sizes', () => {
|
|
115
|
+
const result = requestSize(
|
|
116
|
+
{ 'content-length': '1024' },
|
|
117
|
+
{ 'content-length': '2048' },
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(result).toEqual({
|
|
121
|
+
'http.request.body.size': 1024,
|
|
122
|
+
'http.response.body.size': 2048,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('handles request-only size', () => {
|
|
127
|
+
const result = requestSize({ 'content-length': '512' });
|
|
128
|
+
|
|
129
|
+
expect(result).toEqual({ 'http.request.body.size': 512 });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns undefined when no content-length headers', () => {
|
|
133
|
+
expect(requestSize({}, {})).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('ignores non-numeric content-length', () => {
|
|
137
|
+
expect(requestSize({ 'content-length': 'abc' })).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('ignores invalid numeric content-length values', () => {
|
|
141
|
+
expect(requestSize({ 'content-length': '-1' })).toBeUndefined();
|
|
142
|
+
expect(requestSize({ 'content-length': '12.5' })).toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('ignores non-digit numeric formats for content-length', () => {
|
|
146
|
+
expect(requestSize({ 'content-length': '1e3' })).toBeUndefined();
|
|
147
|
+
expect(requestSize({ 'content-length': '+10' })).toBeUndefined();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|