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.
Files changed (69) hide show
  1. package/dist/auto.cjs +2 -2
  2. package/dist/auto.js +1 -1
  3. package/dist/{chunk-3MZJ7Y24.cjs → chunk-6WWXA6IK.cjs} +5 -5
  4. package/dist/{chunk-3MZJ7Y24.cjs.map → chunk-6WWXA6IK.cjs.map} +1 -1
  5. package/dist/{chunk-U4D5IBSB.js → chunk-AIX6BVNN.js} +43 -8
  6. package/dist/chunk-AIX6BVNN.js.map +1 -0
  7. package/dist/{chunk-32AXF4MA.js → chunk-B4CGFDZQ.js} +2 -2
  8. package/dist/{chunk-32AXF4MA.js.map → chunk-B4CGFDZQ.js.map} +1 -1
  9. package/dist/{chunk-QICFEFD6.cjs → chunk-BGO7TZID.cjs} +7 -7
  10. package/dist/{chunk-QICFEFD6.cjs.map → chunk-BGO7TZID.cjs.map} +1 -1
  11. package/dist/{chunk-4TAQQZDU.js → chunk-DMSD5AF3.js} +3 -3
  12. package/dist/{chunk-4TAQQZDU.js.map → chunk-DMSD5AF3.js.map} +1 -1
  13. package/dist/{chunk-U72TGONP.cjs → chunk-FOFBFQES.cjs} +71 -36
  14. package/dist/chunk-FOFBFQES.cjs.map +1 -0
  15. package/dist/{chunk-DQSVSGK3.cjs → chunk-JAX4LFGG.cjs} +13 -13
  16. package/dist/{chunk-DQSVSGK3.cjs.map → chunk-JAX4LFGG.cjs.map} +1 -1
  17. package/dist/{chunk-TGV2XF57.js → chunk-LCXOOJIP.js} +3 -3
  18. package/dist/{chunk-TGV2XF57.js.map → chunk-LCXOOJIP.js.map} +1 -1
  19. package/dist/{chunk-OACAWYLR.js → chunk-LKASEUWE.js} +4 -4
  20. package/dist/{chunk-OACAWYLR.js.map → chunk-LKASEUWE.js.map} +1 -1
  21. package/dist/{chunk-OPCTN527.js → chunk-NMEYVL4L.js} +3 -3
  22. package/dist/{chunk-OPCTN527.js.map → chunk-NMEYVL4L.js.map} +1 -1
  23. package/dist/{chunk-MQH5OOZK.cjs → chunk-PWOECUNT.cjs} +17 -17
  24. package/dist/{chunk-MQH5OOZK.cjs.map → chunk-PWOECUNT.cjs.map} +1 -1
  25. package/dist/{chunk-QJYWKAC5.cjs → chunk-RYVFCHSO.cjs} +2 -2
  26. package/dist/{chunk-QJYWKAC5.cjs.map → chunk-RYVFCHSO.cjs.map} +1 -1
  27. package/dist/{chunk-FZROHTZZ.js → chunk-TEXCI2S6.js} +3 -3
  28. package/dist/{chunk-FZROHTZZ.js.map → chunk-TEXCI2S6.js.map} +1 -1
  29. package/dist/{chunk-4RA6HIYF.cjs → chunk-Z3VD3UQZ.cjs} +5 -5
  30. package/dist/{chunk-4RA6HIYF.cjs.map → chunk-Z3VD3UQZ.cjs.map} +1 -1
  31. package/dist/correlation-id.cjs +10 -10
  32. package/dist/correlation-id.js +2 -2
  33. package/dist/decorators.cjs +4 -4
  34. package/dist/decorators.js +3 -3
  35. package/dist/event.cjs +6 -6
  36. package/dist/event.js +3 -3
  37. package/dist/functional.cjs +11 -11
  38. package/dist/functional.d.cts +13 -0
  39. package/dist/functional.d.ts +13 -0
  40. package/dist/functional.js +3 -3
  41. package/dist/http.cjs +3 -3
  42. package/dist/http.js +2 -2
  43. package/dist/index.cjs +71 -71
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.js +11 -11
  46. package/dist/index.js.map +1 -1
  47. package/dist/instrumentation.cjs +8 -8
  48. package/dist/instrumentation.js +1 -1
  49. package/dist/messaging.cjs +7 -7
  50. package/dist/messaging.js +4 -4
  51. package/dist/semantic-helpers.cjs +8 -8
  52. package/dist/semantic-helpers.js +4 -4
  53. package/dist/webhook.cjs +5 -5
  54. package/dist/webhook.js +3 -3
  55. package/dist/workflow-distributed.cjs +5 -5
  56. package/dist/workflow-distributed.js +3 -3
  57. package/dist/workflow.cjs +8 -8
  58. package/dist/workflow.js +4 -4
  59. package/package.json +9 -9
  60. package/skills/build-audit-trails/SKILL.md +28 -15
  61. package/skills/build-audit-trails/references/framework-wiring.md +12 -3
  62. package/skills/review-otel-patterns/SKILL.md +9 -6
  63. package/src/error-catalog.test.ts +7 -2
  64. package/src/error-catalog.ts +13 -10
  65. package/src/functional.test.ts +51 -0
  66. package/src/functional.ts +78 -12
  67. package/src/init.ts +5 -1
  68. package/dist/chunk-U4D5IBSB.js.map +0 -1
  69. 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 chunkU72TGONP_cjs = require('./chunk-U72TGONP.cjs');
5
+ var chunkFOFBFQES_cjs = require('./chunk-FOFBFQES.cjs');
6
6
  require('./chunk-2GIBANLB.cjs');
7
7
  require('./chunk-VQTCQKHQ.cjs');
8
- require('./chunk-DQSVSGK3.cjs');
9
- require('./chunk-QJYWKAC5.cjs');
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 chunkU72TGONP_cjs.trace(
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 chunkU72TGONP_cjs.trace(
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-U4D5IBSB.js';
3
+ import { trace } from './chunk-AIX6BVNN.js';
4
4
  import './chunk-HLZ7H3VZ.js';
5
5
  import './chunk-SEO6NAQT.js';
6
- import './chunk-TGV2XF57.js';
7
- import './chunk-32AXF4MA.js';
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 chunk4RA6HIYF_cjs = require('./chunk-4RA6HIYF.cjs');
3
+ var chunkZ3VD3UQZ_cjs = require('./chunk-Z3VD3UQZ.cjs');
4
4
  require('./chunk-4P6ZOARG.cjs');
5
- require('./chunk-U72TGONP.cjs');
5
+ require('./chunk-FOFBFQES.cjs');
6
6
  require('./chunk-2GIBANLB.cjs');
7
7
  require('./chunk-VQTCQKHQ.cjs');
8
- require('./chunk-DQSVSGK3.cjs');
9
- require('./chunk-QJYWKAC5.cjs');
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 chunk4RA6HIYF_cjs.getCurrentWorkflowContext; }
27
+ get: function () { return chunkZ3VD3UQZ_cjs.getCurrentWorkflowContext; }
28
28
  });
29
29
  Object.defineProperty(exports, "isInWorkflow", {
30
30
  enumerable: true,
31
- get: function () { return chunk4RA6HIYF_cjs.isInWorkflow; }
31
+ get: function () { return chunkZ3VD3UQZ_cjs.isInWorkflow; }
32
32
  });
33
33
  Object.defineProperty(exports, "traceStep", {
34
34
  enumerable: true,
35
- get: function () { return chunk4RA6HIYF_cjs.traceStep; }
35
+ get: function () { return chunkZ3VD3UQZ_cjs.traceStep; }
36
36
  });
37
37
  Object.defineProperty(exports, "traceWorkflow", {
38
38
  enumerable: true,
39
- get: function () { return chunk4RA6HIYF_cjs.traceWorkflow; }
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-FZROHTZZ.js';
1
+ export { getCurrentWorkflowContext, isInWorkflow, traceStep, traceWorkflow } from './chunk-TEXCI2S6.js';
2
2
  import './chunk-KIL5CUN6.js';
3
- import './chunk-U4D5IBSB.js';
3
+ import './chunk-AIX6BVNN.js';
4
4
  import './chunk-HLZ7H3VZ.js';
5
5
  import './chunk-SEO6NAQT.js';
6
- import './chunk-TGV2XF57.js';
7
- import './chunk-32AXF4MA.js';
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.0",
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.2",
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.33",
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.7.0",
336
- "@typescript-eslint/eslint-plugin": "^8.59.3",
337
- "@typescript-eslint/parser": "^8.59.3",
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.21.0",
344
+ "tsx": "^4.22.3",
345
345
  "typescript": "^6.0.3",
346
- "typescript-eslint": "^8.59.3",
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.6",
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 | 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) |
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
- { action: 'user.delete', resource: 'user', actorId: 'usr_42', category: 'admin' },
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({ action: 'secret.read', resource: 'sec_abc', actorId: 'usr_42' });
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({ status: 403 });
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(collector.getSpansByAttributes({ 'audit.outcome': 'allow' })).toHaveLength(1);
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(req: Request, { params }: { params: { id: string } }) {
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 () => db.user.delete({ where: { id: (request.params as { id: string }).id } }),
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
- { action: 'data.purge', resource: 'expired-sessions', actorId: 'system:cron', category: 'maintenance' },
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 | 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 |
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<{ user?: { plan?: string } }, { tier: string }>({
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: ({ available, required }: { available: number; required: number }) =>
21
- `Insufficient funds: $${available} of $${required}`,
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
  },
@@ -30,7 +30,10 @@
30
30
  * ```
31
31
  */
32
32
 
33
- import { createStructuredError, type StructuredError } from './structured-error';
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> = ParamsOf<E> extends void
73
- ? [options?: ErrorBuildOptions]
74
- : [params: ParamsOf<E>, options?: ErrorBuildOptions];
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
- usesParams ? maybeOptions : paramsOrOptions
132
- ) as ErrorBuildOptions | undefined;
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> = ParamsOf<E> extends void
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> {
@@ -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 = options.attributesFromArgs
846
- ? options.attributesFromArgs(args)
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 = options.attributesFromResult
883
- ? options.attributesFromResult(result)
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 = options.attributesFromArgs
1159
- ? options.attributesFromArgs(args)
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 = options.attributesFromResult
1196
- ? options.attributesFromResult(result)
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: AttributeRedactorConfig | AttributeRedactorPreset | false | undefined,
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;