autotel 3.3.0 → 3.4.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/dist/auto.cjs +2 -2
- package/dist/auto.js +1 -1
- package/dist/{chunk-3MZJ7Y24.cjs → chunk-6WWXA6IK.cjs} +5 -5
- package/dist/{chunk-3MZJ7Y24.cjs.map → chunk-6WWXA6IK.cjs.map} +1 -1
- package/dist/{chunk-U4D5IBSB.js → chunk-AIX6BVNN.js} +43 -8
- package/dist/chunk-AIX6BVNN.js.map +1 -0
- package/dist/{chunk-32AXF4MA.js → chunk-B4CGFDZQ.js} +2 -2
- package/dist/{chunk-32AXF4MA.js.map → chunk-B4CGFDZQ.js.map} +1 -1
- package/dist/{chunk-QICFEFD6.cjs → chunk-BGO7TZID.cjs} +7 -7
- package/dist/{chunk-QICFEFD6.cjs.map → chunk-BGO7TZID.cjs.map} +1 -1
- package/dist/{chunk-4TAQQZDU.js → chunk-DMSD5AF3.js} +3 -3
- package/dist/{chunk-4TAQQZDU.js.map → chunk-DMSD5AF3.js.map} +1 -1
- package/dist/{chunk-U72TGONP.cjs → chunk-FOFBFQES.cjs} +71 -36
- package/dist/chunk-FOFBFQES.cjs.map +1 -0
- package/dist/{chunk-DQSVSGK3.cjs → chunk-JAX4LFGG.cjs} +13 -13
- package/dist/{chunk-DQSVSGK3.cjs.map → chunk-JAX4LFGG.cjs.map} +1 -1
- package/dist/{chunk-TGV2XF57.js → chunk-LCXOOJIP.js} +3 -3
- package/dist/{chunk-TGV2XF57.js.map → chunk-LCXOOJIP.js.map} +1 -1
- package/dist/{chunk-OACAWYLR.js → chunk-LKASEUWE.js} +4 -4
- package/dist/{chunk-OACAWYLR.js.map → chunk-LKASEUWE.js.map} +1 -1
- package/dist/{chunk-OPCTN527.js → chunk-NMEYVL4L.js} +3 -3
- package/dist/{chunk-OPCTN527.js.map → chunk-NMEYVL4L.js.map} +1 -1
- package/dist/{chunk-MQH5OOZK.cjs → chunk-PWOECUNT.cjs} +17 -17
- package/dist/{chunk-MQH5OOZK.cjs.map → chunk-PWOECUNT.cjs.map} +1 -1
- package/dist/{chunk-QJYWKAC5.cjs → chunk-RYVFCHSO.cjs} +2 -2
- package/dist/{chunk-QJYWKAC5.cjs.map → chunk-RYVFCHSO.cjs.map} +1 -1
- package/dist/{chunk-FZROHTZZ.js → chunk-TEXCI2S6.js} +3 -3
- package/dist/{chunk-FZROHTZZ.js.map → chunk-TEXCI2S6.js.map} +1 -1
- package/dist/{chunk-4RA6HIYF.cjs → chunk-Z3VD3UQZ.cjs} +5 -5
- package/dist/{chunk-4RA6HIYF.cjs.map → chunk-Z3VD3UQZ.cjs.map} +1 -1
- package/dist/correlation-id.cjs +10 -10
- package/dist/correlation-id.js +2 -2
- package/dist/decorators.cjs +4 -4
- package/dist/decorators.js +3 -3
- package/dist/event.cjs +6 -6
- package/dist/event.js +3 -3
- package/dist/functional.cjs +11 -11
- package/dist/functional.d.cts +13 -0
- package/dist/functional.d.ts +13 -0
- package/dist/functional.js +3 -3
- package/dist/http.cjs +3 -3
- package/dist/http.js +2 -2
- package/dist/index.cjs +71 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +11 -11
- package/dist/index.js.map +1 -1
- package/dist/instrumentation.cjs +8 -8
- package/dist/instrumentation.js +1 -1
- package/dist/messaging.cjs +7 -7
- package/dist/messaging.js +4 -4
- package/dist/semantic-helpers.cjs +8 -8
- package/dist/semantic-helpers.js +4 -4
- package/dist/webhook.cjs +5 -5
- 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/package.json +9 -9
- package/skills/build-audit-trails/SKILL.md +28 -15
- package/skills/build-audit-trails/references/framework-wiring.md +12 -3
- package/skills/review-otel-patterns/SKILL.md +9 -6
- package/src/error-catalog.test.ts +7 -2
- package/src/error-catalog.ts +13 -10
- package/src/functional.test.ts +51 -0
- package/src/functional.ts +78 -12
- package/src/init.ts +5 -1
- package/dist/chunk-U4D5IBSB.js.map +0 -1
- package/dist/chunk-U72TGONP.cjs.map +0 -1
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
var chunk4P6ZOARG_cjs = require('./chunk-4P6ZOARG.cjs');
|
|
4
4
|
var chunkINJD3G4K_cjs = require('./chunk-INJD3G4K.cjs');
|
|
5
|
-
var
|
|
5
|
+
var chunkFOFBFQES_cjs = require('./chunk-FOFBFQES.cjs');
|
|
6
6
|
require('./chunk-2GIBANLB.cjs');
|
|
7
7
|
require('./chunk-VQTCQKHQ.cjs');
|
|
8
|
-
require('./chunk-
|
|
9
|
-
require('./chunk-
|
|
8
|
+
require('./chunk-JAX4LFGG.cjs');
|
|
9
|
+
require('./chunk-RYVFCHSO.cjs');
|
|
10
10
|
require('./chunk-FEEVB2GV.cjs');
|
|
11
11
|
require('./chunk-CEAQK2QY.cjs');
|
|
12
12
|
require('./chunk-ZNMBW67B.cjs');
|
|
@@ -58,7 +58,7 @@ var WorkflowBaggage = chunkINJD3G4K_cjs.createSafeBaggageSchema(workflowBaggageF
|
|
|
58
58
|
function traceDistributedWorkflow(config) {
|
|
59
59
|
const spanName = `workflow.${config.name}`;
|
|
60
60
|
return (fnFactory) => {
|
|
61
|
-
return
|
|
61
|
+
return chunkFOFBFQES_cjs.trace(
|
|
62
62
|
{ name: spanName, spanKind: api.SpanKind.INTERNAL },
|
|
63
63
|
(baseCtx) => {
|
|
64
64
|
return async (...args) => {
|
|
@@ -157,7 +157,7 @@ function traceDistributedWorkflow(config) {
|
|
|
157
157
|
function traceDistributedStep(config) {
|
|
158
158
|
const spanName = `workflow.step.${config.name}`;
|
|
159
159
|
return (fnFactory) => {
|
|
160
|
-
return
|
|
160
|
+
return chunkFOFBFQES_cjs.trace(
|
|
161
161
|
{ name: spanName, spanKind: api.SpanKind.INTERNAL },
|
|
162
162
|
(baseCtx) => {
|
|
163
163
|
return async (...args) => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { emitCorrelatedEvent } from './chunk-KIL5CUN6.js';
|
|
2
2
|
import { createSafeBaggageSchema } from './chunk-4IFSYQVX.js';
|
|
3
|
-
import { trace } from './chunk-
|
|
3
|
+
import { trace } from './chunk-AIX6BVNN.js';
|
|
4
4
|
import './chunk-HLZ7H3VZ.js';
|
|
5
5
|
import './chunk-SEO6NAQT.js';
|
|
6
|
-
import './chunk-
|
|
7
|
-
import './chunk-
|
|
6
|
+
import './chunk-LCXOOJIP.js';
|
|
7
|
+
import './chunk-B4CGFDZQ.js';
|
|
8
8
|
import './chunk-643PQG3Y.js';
|
|
9
9
|
import './chunk-A4E5AQFK.js';
|
|
10
10
|
import './chunk-WGWSHJ2N.js';
|
package/dist/workflow.cjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkZ3VD3UQZ_cjs = require('./chunk-Z3VD3UQZ.cjs');
|
|
4
4
|
require('./chunk-4P6ZOARG.cjs');
|
|
5
|
-
require('./chunk-
|
|
5
|
+
require('./chunk-FOFBFQES.cjs');
|
|
6
6
|
require('./chunk-2GIBANLB.cjs');
|
|
7
7
|
require('./chunk-VQTCQKHQ.cjs');
|
|
8
|
-
require('./chunk-
|
|
9
|
-
require('./chunk-
|
|
8
|
+
require('./chunk-JAX4LFGG.cjs');
|
|
9
|
+
require('./chunk-RYVFCHSO.cjs');
|
|
10
10
|
require('./chunk-FEEVB2GV.cjs');
|
|
11
11
|
require('./chunk-CEAQK2QY.cjs');
|
|
12
12
|
require('./chunk-ZNMBW67B.cjs');
|
|
@@ -24,19 +24,19 @@ require('./chunk-YREV3LGG.cjs');
|
|
|
24
24
|
|
|
25
25
|
Object.defineProperty(exports, "getCurrentWorkflowContext", {
|
|
26
26
|
enumerable: true,
|
|
27
|
-
get: function () { return
|
|
27
|
+
get: function () { return chunkZ3VD3UQZ_cjs.getCurrentWorkflowContext; }
|
|
28
28
|
});
|
|
29
29
|
Object.defineProperty(exports, "isInWorkflow", {
|
|
30
30
|
enumerable: true,
|
|
31
|
-
get: function () { return
|
|
31
|
+
get: function () { return chunkZ3VD3UQZ_cjs.isInWorkflow; }
|
|
32
32
|
});
|
|
33
33
|
Object.defineProperty(exports, "traceStep", {
|
|
34
34
|
enumerable: true,
|
|
35
|
-
get: function () { return
|
|
35
|
+
get: function () { return chunkZ3VD3UQZ_cjs.traceStep; }
|
|
36
36
|
});
|
|
37
37
|
Object.defineProperty(exports, "traceWorkflow", {
|
|
38
38
|
enumerable: true,
|
|
39
|
-
get: function () { return
|
|
39
|
+
get: function () { return chunkZ3VD3UQZ_cjs.traceWorkflow; }
|
|
40
40
|
});
|
|
41
41
|
//# sourceMappingURL=workflow.cjs.map
|
|
42
42
|
//# sourceMappingURL=workflow.cjs.map
|
package/dist/workflow.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { getCurrentWorkflowContext, isInWorkflow, traceStep, traceWorkflow } from './chunk-
|
|
1
|
+
export { getCurrentWorkflowContext, isInWorkflow, traceStep, traceWorkflow } from './chunk-TEXCI2S6.js';
|
|
2
2
|
import './chunk-KIL5CUN6.js';
|
|
3
|
-
import './chunk-
|
|
3
|
+
import './chunk-AIX6BVNN.js';
|
|
4
4
|
import './chunk-HLZ7H3VZ.js';
|
|
5
5
|
import './chunk-SEO6NAQT.js';
|
|
6
|
-
import './chunk-
|
|
7
|
-
import './chunk-
|
|
6
|
+
import './chunk-LCXOOJIP.js';
|
|
7
|
+
import './chunk-B4CGFDZQ.js';
|
|
8
8
|
import './chunk-643PQG3Y.js';
|
|
9
9
|
import './chunk-A4E5AQFK.js';
|
|
10
10
|
import './chunk-WGWSHJ2N.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autotel",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Write Once, Observe Anywhere",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
}
|
|
318
318
|
},
|
|
319
319
|
"devDependencies": {
|
|
320
|
-
"@arethetypeswrong/cli": "^0.18.
|
|
320
|
+
"@arethetypeswrong/cli": "^0.18.3",
|
|
321
321
|
"@edge-runtime/vm": "^5.0.0",
|
|
322
322
|
"@opentelemetry/auto-instrumentations-node": "^0.76.0",
|
|
323
323
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
@@ -328,25 +328,25 @@
|
|
|
328
328
|
"@opentelemetry/resource-detector-container": "^0.8.9",
|
|
329
329
|
"@opentelemetry/resource-detector-gcp": "^0.53.0",
|
|
330
330
|
"@opentelemetry/sdk-trace-node": "^2.7.1",
|
|
331
|
-
"@swc/core": "^1.15.
|
|
331
|
+
"@swc/core": "^1.15.40",
|
|
332
332
|
"@total-typescript/ts-reset": "^0.6.1",
|
|
333
333
|
"@total-typescript/tsconfig": "^1.0.4",
|
|
334
334
|
"@types/eslint-config-prettier": "^6.11.3",
|
|
335
|
-
"@types/node": "^25.
|
|
336
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
337
|
-
"@typescript-eslint/parser": "^8.
|
|
335
|
+
"@types/node": "^25.9.1",
|
|
336
|
+
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
|
337
|
+
"@typescript-eslint/parser": "^8.60.0",
|
|
338
338
|
"eslint-config-prettier": "^10.1.8",
|
|
339
339
|
"eslint-plugin-unicorn": "^64.0.0",
|
|
340
340
|
"pino": "^10.3.1",
|
|
341
341
|
"prettier": "^3.8.3",
|
|
342
342
|
"rimraf": "^6.1.3",
|
|
343
343
|
"tsup": "^8.5.1",
|
|
344
|
-
"tsx": "^4.
|
|
344
|
+
"tsx": "^4.22.3",
|
|
345
345
|
"typescript": "^6.0.3",
|
|
346
|
-
"typescript-eslint": "^8.
|
|
346
|
+
"typescript-eslint": "^8.60.0",
|
|
347
347
|
"unplugin-swc": "^1.5.9",
|
|
348
348
|
"vite-tsconfig-paths": "^6.1.1",
|
|
349
|
-
"vitest": "^4.1.
|
|
349
|
+
"vitest": "^4.1.7",
|
|
350
350
|
"vitest-mock-extended": "^4.0.0",
|
|
351
351
|
"winston": "^3.19.0",
|
|
352
352
|
"yaml": "^2.9.0"
|
|
@@ -31,17 +31,17 @@ autotel lets you express both with the same primitive — a span — but you sho
|
|
|
31
31
|
|
|
32
32
|
## Quick reference
|
|
33
33
|
|
|
34
|
-
| Situation
|
|
35
|
-
|
|
|
36
|
-
| Wrap an action so success/failure is audited + kept
|
|
37
|
-
| Tag the active span with `audit.*` attributes only
|
|
38
|
-
| Make sure an audit span survives tail sampling
|
|
39
|
-
| Record an authorization denial
|
|
40
|
-
| Full control / framework-agnostic span helper
|
|
41
|
-
| Keep audit data off ops dashboards
|
|
42
|
-
| Prove a record was not altered
|
|
43
|
-
| Honor a GDPR erasure request on an append-only log
|
|
44
|
-
| Assert the trail in tests
|
|
34
|
+
| Situation | Use |
|
|
35
|
+
| --------------------------------------------------- | -------------------------------------------------------- |
|
|
36
|
+
| Wrap an action so success/failure is audited + kept | `withAudit(metadata, fn)` from `autotel-audit` |
|
|
37
|
+
| Tag the active span with `audit.*` attributes only | `setAuditAttributes(metadata)` |
|
|
38
|
+
| Make sure an audit span survives tail sampling | `forceKeepAuditEvent()` |
|
|
39
|
+
| Record an authorization denial | `audit({ …, outcome: 'deny', reason })` (Step 1 helper) |
|
|
40
|
+
| Full control / framework-agnostic span helper | hand-rolled `audit()` (Step 1) |
|
|
41
|
+
| Keep audit data off ops dashboards | `FilteringSpanProcessor` split (Step 3) |
|
|
42
|
+
| Prove a record was not altered | HMAC or hash-chain signature (Step 4) |
|
|
43
|
+
| Honor a GDPR erasure request on an append-only log | crypto-shredding (Step 6.5) |
|
|
44
|
+
| Assert the trail in tests | `createTraceCollector()` from `autotel/testing` (Step 8) |
|
|
45
45
|
|
|
46
46
|
## The shortest path: the `autotel-audit` package
|
|
47
47
|
|
|
@@ -57,12 +57,21 @@ import {
|
|
|
57
57
|
// Wrap the action: sets audit.* attributes, force-keeps past tail sampling,
|
|
58
58
|
// and tags outcome 'success' / 'failure' automatically.
|
|
59
59
|
await withAudit(
|
|
60
|
-
{
|
|
60
|
+
{
|
|
61
|
+
action: 'user.delete',
|
|
62
|
+
resource: 'user',
|
|
63
|
+
actorId: 'usr_42',
|
|
64
|
+
category: 'admin',
|
|
65
|
+
},
|
|
61
66
|
async () => db.user.delete({ where: { id } }),
|
|
62
67
|
);
|
|
63
68
|
|
|
64
69
|
// Or tag the current span yourself and opt out of sampling:
|
|
65
|
-
setAuditAttributes({
|
|
70
|
+
setAuditAttributes({
|
|
71
|
+
action: 'secret.read',
|
|
72
|
+
resource: 'sec_abc',
|
|
73
|
+
actorId: 'usr_42',
|
|
74
|
+
});
|
|
66
75
|
forceKeepAuditEvent();
|
|
67
76
|
```
|
|
68
77
|
|
|
@@ -381,7 +390,9 @@ describe('audit trail', () => {
|
|
|
381
390
|
});
|
|
382
391
|
|
|
383
392
|
it('records a denial with reason and actor', async () => {
|
|
384
|
-
await expect(deleteUser(forbiddenReq)).rejects.toMatchObject({
|
|
393
|
+
await expect(deleteUser(forbiddenReq)).rejects.toMatchObject({
|
|
394
|
+
status: 403,
|
|
395
|
+
});
|
|
385
396
|
|
|
386
397
|
const [span] = collector.getSpansByAttributes({ 'audit.outcome': 'deny' });
|
|
387
398
|
expect(span).toBeDefined();
|
|
@@ -392,7 +403,9 @@ describe('audit trail', () => {
|
|
|
392
403
|
|
|
393
404
|
it('records the success path on allow', async () => {
|
|
394
405
|
await deleteUser(allowedReq);
|
|
395
|
-
expect(
|
|
406
|
+
expect(
|
|
407
|
+
collector.getSpansByAttributes({ 'audit.outcome': 'allow' }),
|
|
408
|
+
).toHaveLength(1);
|
|
396
409
|
});
|
|
397
410
|
});
|
|
398
411
|
```
|
|
@@ -10,7 +10,10 @@ The rule across every framework is the same: wrap the authorization decision so
|
|
|
10
10
|
// app/admin/users/[id]/route.ts
|
|
11
11
|
import { withAuthz } from '@/lib/audit';
|
|
12
12
|
|
|
13
|
-
export async function DELETE(
|
|
13
|
+
export async function DELETE(
|
|
14
|
+
req: Request,
|
|
15
|
+
{ params }: { params: { id: string } },
|
|
16
|
+
) {
|
|
14
17
|
return withAuthz(
|
|
15
18
|
{
|
|
16
19
|
action: 'user.delete',
|
|
@@ -101,7 +104,8 @@ fastify.delete('/admin/users/:id', async (request, reply) => {
|
|
|
101
104
|
actor: { id: request.user.id, role: request.user.role },
|
|
102
105
|
},
|
|
103
106
|
async () => ({ allow: request.user.role === 'admin' }),
|
|
104
|
-
async () =>
|
|
107
|
+
async () =>
|
|
108
|
+
db.user.delete({ where: { id: (request.params as { id: string }).id } }),
|
|
105
109
|
);
|
|
106
110
|
return { ok: true };
|
|
107
111
|
});
|
|
@@ -180,7 +184,12 @@ import { withAudit } from 'autotel-audit';
|
|
|
180
184
|
|
|
181
185
|
export const nightlyPurge = trace(async function nightlyPurge() {
|
|
182
186
|
await withAudit(
|
|
183
|
-
{
|
|
187
|
+
{
|
|
188
|
+
action: 'data.purge',
|
|
189
|
+
resource: 'expired-sessions',
|
|
190
|
+
actorId: 'system:cron',
|
|
191
|
+
category: 'maintenance',
|
|
192
|
+
},
|
|
184
193
|
async () => sessions.purgeExpired(),
|
|
185
194
|
);
|
|
186
195
|
});
|
|
@@ -237,11 +237,11 @@ All options work with `init()`, framework adapters, and `wrapModule` / `defineWo
|
|
|
237
237
|
|
|
238
238
|
Enrichers turn raw request data into standard, low-cardinality span attributes. Import the helpers from `autotel/enrichers` and spread their output onto the active span or request logger. Each returns `undefined` when there is nothing to add, so spreading is safe.
|
|
239
239
|
|
|
240
|
-
| Helper
|
|
241
|
-
|
|
|
242
|
-
| `userAgent(headers)`
|
|
243
|
-
| `geo(headers)`
|
|
244
|
-
| `requestSize(reqHeaders, resHeaders?)`
|
|
240
|
+
| Helper | Returns attributes | Source |
|
|
241
|
+
| -------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------- |
|
|
242
|
+
| `userAgent(headers)` | `user_agent.raw`, `user_agent.browser`, `user_agent.os`, `user_agent.device` | `user-agent` request header |
|
|
243
|
+
| `geo(headers)` | `geo.country`, `geo.region`, `geo.city`, `geo.latitude`, `geo.longitude` | Vercel / Cloudflare geo headers |
|
|
244
|
+
| `requestSize(reqHeaders, resHeaders?)` | `http.request.body.size`, `http.response.body.size` | `content-length` headers |
|
|
245
245
|
|
|
246
246
|
```typescript
|
|
247
247
|
import { userAgent, geo, requestSize } from 'autotel/enrichers';
|
|
@@ -262,7 +262,10 @@ For your own derived fields on a request's wide event, build a reusable enricher
|
|
|
262
262
|
import { defineEnricher } from 'autotel';
|
|
263
263
|
|
|
264
264
|
// Merge a derived, low-cardinality object into event.user on each request.
|
|
265
|
-
const enrichTier = defineEnricher<
|
|
265
|
+
const enrichTier = defineEnricher<
|
|
266
|
+
{ user?: { plan?: string } },
|
|
267
|
+
{ tier: string }
|
|
268
|
+
>({
|
|
266
269
|
name: 'user-tier',
|
|
267
270
|
field: 'user',
|
|
268
271
|
compute: ({ event }) => ({ tier: event.user?.plan ?? 'anonymous' }),
|
|
@@ -17,8 +17,13 @@ describe('defineErrorCatalog', () => {
|
|
|
17
17
|
},
|
|
18
18
|
INSUFFICIENT_FUNDS: {
|
|
19
19
|
status: 402,
|
|
20
|
-
message: ({
|
|
21
|
-
|
|
20
|
+
message: ({
|
|
21
|
+
available,
|
|
22
|
+
required,
|
|
23
|
+
}: {
|
|
24
|
+
available: number;
|
|
25
|
+
required: number;
|
|
26
|
+
}) => `Insufficient funds: $${available} of $${required}`,
|
|
22
27
|
why: ({ required }: { available: number; required: number }) =>
|
|
23
28
|
`Needs $${required}`,
|
|
24
29
|
},
|
package/src/error-catalog.ts
CHANGED
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
createStructuredError,
|
|
35
|
+
type StructuredError,
|
|
36
|
+
} from './structured-error';
|
|
34
37
|
|
|
35
38
|
const catalogCodeKey = Symbol.for('autotel.catalog.code');
|
|
36
39
|
|
|
@@ -69,9 +72,10 @@ type ParamsOf<E> = E extends { message: (params: infer P) => string }
|
|
|
69
72
|
? P
|
|
70
73
|
: void;
|
|
71
74
|
|
|
72
|
-
type BuilderArgs<E extends ErrorCatalogEntry> =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
type BuilderArgs<E extends ErrorCatalogEntry> =
|
|
76
|
+
ParamsOf<E> extends void
|
|
77
|
+
? [options?: ErrorBuildOptions]
|
|
78
|
+
: [params: ParamsOf<E>, options?: ErrorBuildOptions];
|
|
75
79
|
|
|
76
80
|
/** A callable error factory produced by {@link defineErrorCatalog}. */
|
|
77
81
|
export interface ErrorBuilder<E extends ErrorCatalogEntry> {
|
|
@@ -127,9 +131,9 @@ export function defineErrorCatalog<
|
|
|
127
131
|
maybeOptions?: ErrorBuildOptions,
|
|
128
132
|
): StructuredError => {
|
|
129
133
|
const params = usesParams ? paramsOrOptions : undefined;
|
|
130
|
-
const options = (
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
const options = (usesParams ? maybeOptions : paramsOrOptions) as
|
|
135
|
+
| ErrorBuildOptions
|
|
136
|
+
| undefined;
|
|
133
137
|
|
|
134
138
|
const message =
|
|
135
139
|
typeof entry.message === 'function'
|
|
@@ -200,9 +204,8 @@ export interface AuditAction {
|
|
|
200
204
|
readonly message?: string;
|
|
201
205
|
}
|
|
202
206
|
|
|
203
|
-
type AuditDescriptorArgs<E extends AuditCatalogEntry> =
|
|
204
|
-
? []
|
|
205
|
-
: [params: ParamsOf<E>];
|
|
207
|
+
type AuditDescriptorArgs<E extends AuditCatalogEntry> =
|
|
208
|
+
ParamsOf<E> extends void ? [] : [params: ParamsOf<E>];
|
|
206
209
|
|
|
207
210
|
/** A callable audit-action descriptor produced by {@link defineAuditCatalog}. */
|
|
208
211
|
export interface AuditDescriptor<E extends AuditCatalogEntry> {
|
package/src/functional.test.ts
CHANGED
|
@@ -337,6 +337,57 @@ describe('Functional API', () => {
|
|
|
337
337
|
expect(spans[0]!.attributes['userId']).toBe('456');
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
+
it('captures input/output as autotel.input/output when opted in', async () => {
|
|
341
|
+
const collector = createTraceCollector();
|
|
342
|
+
|
|
343
|
+
const calc = traceOptionsFactory(
|
|
344
|
+
{ name: 'calc', captureInput: true, captureOutput: true },
|
|
345
|
+
(_ctx: TraceContext) => async (a: number, b: number) => a + b,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
await calc(2, 3);
|
|
349
|
+
|
|
350
|
+
const span = collector.getSpans()[0]!;
|
|
351
|
+
// Multiple args captured as an array; single value would be captured directly.
|
|
352
|
+
expect(span.attributes['autotel.input']).toBe('[2,3]');
|
|
353
|
+
expect(span.attributes['autotel.output']).toBe('5');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('does not capture input/output by default', async () => {
|
|
357
|
+
const collector = createTraceCollector();
|
|
358
|
+
|
|
359
|
+
const calc = traceOptionsFactory(
|
|
360
|
+
{ name: 'calc-default' },
|
|
361
|
+
(_ctx: TraceContext) => async (a: number, b: number) => a + b,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await calc(2, 3);
|
|
365
|
+
|
|
366
|
+
const span = collector.getSpans()[0]!;
|
|
367
|
+
expect(span.attributes['autotel.input']).toBeUndefined();
|
|
368
|
+
expect(span.attributes['autotel.output']).toBeUndefined();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('captures a single argument directly (not wrapped in an array)', async () => {
|
|
372
|
+
const collector = createTraceCollector();
|
|
373
|
+
|
|
374
|
+
const load = traceOptionsFactory(
|
|
375
|
+
{ name: 'load', captureInput: true, captureOutput: true },
|
|
376
|
+
(_ctx: TraceContext) => async (req: { userId: string }) => ({
|
|
377
|
+
holdings: 3,
|
|
378
|
+
userId: req.userId,
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
await load({ userId: 'anon' });
|
|
383
|
+
|
|
384
|
+
const span = collector.getSpans()[0]!;
|
|
385
|
+
expect(span.attributes['autotel.input']).toBe('{"userId":"anon"}');
|
|
386
|
+
expect(span.attributes['autotel.output']).toBe(
|
|
387
|
+
'{"holdings":3,"userId":"anon"}',
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
340
391
|
it('should respect NeverSampler', async () => {
|
|
341
392
|
const collector = createTraceCollector();
|
|
342
393
|
|
package/src/functional.ts
CHANGED
|
@@ -415,6 +415,21 @@ export interface TracingOptions<
|
|
|
415
415
|
*/
|
|
416
416
|
attributesFromResult?: (result: TReturn) => Record<string, unknown>;
|
|
417
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Capture the function arguments onto the span as `autotel.input`
|
|
420
|
+
* (JSON, truncated). One arg is captured directly; multiple are captured as
|
|
421
|
+
* an array. Off by default — opt in per call. Tools (visualizers, devtools)
|
|
422
|
+
* read this alongside `ai.toolCall.args` to show function I/O uniformly.
|
|
423
|
+
* Avoid on args with secrets/PII, or pair with a redacting processor.
|
|
424
|
+
*/
|
|
425
|
+
captureInput?: boolean;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Capture the function return value onto the span as `autotel.output`
|
|
429
|
+
* (JSON, truncated). Off by default. Same caveats as {@link captureInput}.
|
|
430
|
+
*/
|
|
431
|
+
captureOutput?: boolean;
|
|
432
|
+
|
|
418
433
|
/**
|
|
419
434
|
* Start a new root span instead of creating a child
|
|
420
435
|
* Useful for serverless entry points
|
|
@@ -494,6 +509,45 @@ function createDummyCtx<
|
|
|
494
509
|
} as unknown as TraceContext<TBaggage>;
|
|
495
510
|
}
|
|
496
511
|
|
|
512
|
+
/** Attribute keys for opt-in function I/O capture (see TracingOptions). */
|
|
513
|
+
const AUTOTEL_INPUT_ATTR = 'autotel.input';
|
|
514
|
+
const AUTOTEL_OUTPUT_ATTR = 'autotel.output';
|
|
515
|
+
const CAPTURE_MAX_CHARS = 4096;
|
|
516
|
+
|
|
517
|
+
/** JSON-serialize a captured value, defensively (truncate, swallow cycles). */
|
|
518
|
+
function serializeCapture(value: unknown): string | undefined {
|
|
519
|
+
if (value === undefined) return undefined;
|
|
520
|
+
try {
|
|
521
|
+
const json = typeof value === 'string' ? value : JSON.stringify(value);
|
|
522
|
+
if (json === undefined) return undefined;
|
|
523
|
+
return json.length > CAPTURE_MAX_CHARS
|
|
524
|
+
? `${json.slice(0, CAPTURE_MAX_CHARS)}…[truncated]`
|
|
525
|
+
: json;
|
|
526
|
+
} catch {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** `autotel.input` from args (single arg captured directly, else the array). */
|
|
532
|
+
function captureInputAttrs(
|
|
533
|
+
args: unknown[],
|
|
534
|
+
enabled?: boolean,
|
|
535
|
+
): Record<string, unknown> {
|
|
536
|
+
if (!enabled) return {};
|
|
537
|
+
const s = serializeCapture(args.length === 1 ? args[0] : args);
|
|
538
|
+
return s === undefined ? {} : { [AUTOTEL_INPUT_ATTR]: s };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** `autotel.output` from the return value. */
|
|
542
|
+
function captureOutputAttrs(
|
|
543
|
+
result: unknown,
|
|
544
|
+
enabled?: boolean,
|
|
545
|
+
): Record<string, unknown> {
|
|
546
|
+
if (!enabled) return {};
|
|
547
|
+
const s = serializeCapture(result);
|
|
548
|
+
return s === undefined ? {} : { [AUTOTEL_OUTPUT_ATTR]: s };
|
|
549
|
+
}
|
|
550
|
+
|
|
497
551
|
function isAsyncFunction(fn: unknown): boolean {
|
|
498
552
|
return typeof fn === 'function' && fn.constructor?.name === 'AsyncFunction';
|
|
499
553
|
}
|
|
@@ -842,9 +896,12 @@ function wrapWithTracing<TArgs extends unknown[], TReturn>(
|
|
|
842
896
|
|
|
843
897
|
const ctxValue = createTraceContext(span);
|
|
844
898
|
const fn = fnFactory(ctxValue);
|
|
845
|
-
const argsAttributes =
|
|
846
|
-
|
|
847
|
-
|
|
899
|
+
const argsAttributes = {
|
|
900
|
+
...captureInputAttrs(args, options.captureInput),
|
|
901
|
+
...(options.attributesFromArgs
|
|
902
|
+
? options.attributesFromArgs(args)
|
|
903
|
+
: {}),
|
|
904
|
+
};
|
|
848
905
|
|
|
849
906
|
const handleTailSampling = (
|
|
850
907
|
success: boolean,
|
|
@@ -879,9 +936,12 @@ function wrapWithTracing<TArgs extends unknown[], TReturn>(
|
|
|
879
936
|
status: 'success',
|
|
880
937
|
});
|
|
881
938
|
|
|
882
|
-
const resultAttributes =
|
|
883
|
-
|
|
884
|
-
|
|
939
|
+
const resultAttributes = {
|
|
940
|
+
...captureOutputAttrs(result, options.captureOutput),
|
|
941
|
+
...(options.attributesFromResult
|
|
942
|
+
? options.attributesFromResult(result)
|
|
943
|
+
: {}),
|
|
944
|
+
};
|
|
885
945
|
|
|
886
946
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
887
947
|
span.setAttributes({
|
|
@@ -1155,9 +1215,12 @@ function wrapWithTracingSync<TArgs extends unknown[], TReturn>(
|
|
|
1155
1215
|
|
|
1156
1216
|
// Extract attributes only when actually tracing
|
|
1157
1217
|
// This avoids expensive preprocessing when sampling rejects the trace
|
|
1158
|
-
const argsAttributes =
|
|
1159
|
-
|
|
1160
|
-
|
|
1218
|
+
const argsAttributes = {
|
|
1219
|
+
...captureInputAttrs(args, options.captureInput),
|
|
1220
|
+
...(options.attributesFromArgs
|
|
1221
|
+
? options.attributesFromArgs(args)
|
|
1222
|
+
: {}),
|
|
1223
|
+
};
|
|
1161
1224
|
|
|
1162
1225
|
const handleTailSampling = (
|
|
1163
1226
|
success: boolean,
|
|
@@ -1192,9 +1255,12 @@ function wrapWithTracingSync<TArgs extends unknown[], TReturn>(
|
|
|
1192
1255
|
status: 'success',
|
|
1193
1256
|
});
|
|
1194
1257
|
|
|
1195
|
-
const resultAttributes =
|
|
1196
|
-
|
|
1197
|
-
|
|
1258
|
+
const resultAttributes = {
|
|
1259
|
+
...captureOutputAttrs(result, options.captureOutput),
|
|
1260
|
+
...(options.attributesFromResult
|
|
1261
|
+
? options.attributesFromResult(result)
|
|
1262
|
+
: {}),
|
|
1263
|
+
};
|
|
1198
1264
|
|
|
1199
1265
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
1200
1266
|
span.setAttributes({
|
package/src/init.ts
CHANGED
|
@@ -1275,7 +1275,11 @@ function wrapLogger(
|
|
|
1275
1275
|
* final default PII redaction is auto-enabled in production.
|
|
1276
1276
|
*/
|
|
1277
1277
|
export function resolveAttributeRedactor(
|
|
1278
|
-
explicit:
|
|
1278
|
+
explicit:
|
|
1279
|
+
| AttributeRedactorConfig
|
|
1280
|
+
| AttributeRedactorPreset
|
|
1281
|
+
| false
|
|
1282
|
+
| undefined,
|
|
1279
1283
|
environment: string,
|
|
1280
1284
|
): AttributeRedactorConfig | AttributeRedactorPreset | undefined {
|
|
1281
1285
|
if (explicit === false) return undefined;
|