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.
Files changed (126) hide show
  1. package/README.md +112 -6
  2. package/bin/intent.js +6 -0
  3. package/dist/auto.cjs +3 -3
  4. package/dist/auto.js +2 -2
  5. package/dist/{chunk-EWH2542B.js → chunk-3AMR5XLZ.js} +3 -3
  6. package/dist/{chunk-EWH2542B.js.map → chunk-3AMR5XLZ.js.map} +1 -1
  7. package/dist/chunk-3QXBFGKP.js +344 -0
  8. package/dist/chunk-3QXBFGKP.js.map +1 -0
  9. package/dist/{chunk-VQFF2WMP.cjs → chunk-3ZFDJJWZ.cjs} +37 -29
  10. package/dist/chunk-3ZFDJJWZ.cjs.map +1 -0
  11. package/dist/{chunk-CQC6RVLR.cjs → chunk-4RZ4JUBY.cjs} +5 -5
  12. package/dist/{chunk-CQC6RVLR.cjs.map → chunk-4RZ4JUBY.cjs.map} +1 -1
  13. package/dist/{chunk-PAVYKPCQ.js → chunk-5XUEHX7J.js} +3 -3
  14. package/dist/{chunk-PAVYKPCQ.js.map → chunk-5XUEHX7J.js.map} +1 -1
  15. package/dist/chunk-6S5RUKU3.cjs +347 -0
  16. package/dist/chunk-6S5RUKU3.cjs.map +1 -0
  17. package/dist/{chunk-BS757SL2.js → chunk-724XLWR3.js} +9 -4
  18. package/dist/chunk-724XLWR3.js.map +1 -0
  19. package/dist/chunk-7EQ4G4SI.cjs +146 -0
  20. package/dist/chunk-7EQ4G4SI.cjs.map +1 -0
  21. package/dist/{chunk-CQP5SQT4.cjs → chunk-AXFWWJF3.cjs} +7 -7
  22. package/dist/{chunk-CQP5SQT4.cjs.map → chunk-AXFWWJF3.cjs.map} +1 -1
  23. package/dist/{chunk-7NH625MS.cjs → chunk-BSZP4URK.cjs} +5 -5
  24. package/dist/{chunk-7NH625MS.cjs.map → chunk-BSZP4URK.cjs.map} +1 -1
  25. package/dist/{chunk-GZFH6P5U.js → chunk-GY4CRZSV.js} +14 -6
  26. package/dist/chunk-GY4CRZSV.js.map +1 -0
  27. package/dist/{chunk-QKUGUDXJ.cjs → chunk-HSEIUH7F.cjs} +10 -5
  28. package/dist/chunk-HSEIUH7F.cjs.map +1 -0
  29. package/dist/{chunk-DTW3WB7Z.js → chunk-IPKXURBW.js} +3 -3
  30. package/dist/{chunk-DTW3WB7Z.js.map → chunk-IPKXURBW.js.map} +1 -1
  31. package/dist/chunk-J7VGRIAJ.js +64 -0
  32. package/dist/chunk-J7VGRIAJ.js.map +1 -0
  33. package/dist/chunk-KFOHQK7X.js +144 -0
  34. package/dist/chunk-KFOHQK7X.js.map +1 -0
  35. package/dist/{chunk-4UYR46UP.cjs → chunk-MSUHW2I4.cjs} +13 -13
  36. package/dist/{chunk-4UYR46UP.cjs.map → chunk-MSUHW2I4.cjs.map} +1 -1
  37. package/dist/chunk-T4B5LB6E.cjs +66 -0
  38. package/dist/chunk-T4B5LB6E.cjs.map +1 -0
  39. package/dist/{chunk-QHT4MUED.js → chunk-WCIIFRGL.js} +3 -3
  40. package/dist/{chunk-QHT4MUED.js.map → chunk-WCIIFRGL.js.map} +1 -1
  41. package/dist/decorators.cjs +3 -3
  42. package/dist/decorators.js +3 -3
  43. package/dist/drain-pipeline.cjs +13 -0
  44. package/dist/drain-pipeline.cjs.map +1 -0
  45. package/dist/drain-pipeline.d.cts +37 -0
  46. package/dist/drain-pipeline.d.ts +37 -0
  47. package/dist/drain-pipeline.js +4 -0
  48. package/dist/drain-pipeline.js.map +1 -0
  49. package/dist/enrichers.cjs +90 -0
  50. package/dist/enrichers.cjs.map +1 -0
  51. package/dist/enrichers.d.cts +23 -0
  52. package/dist/enrichers.d.ts +23 -0
  53. package/dist/enrichers.js +86 -0
  54. package/dist/enrichers.js.map +1 -0
  55. package/dist/event.cjs +6 -6
  56. package/dist/event.js +3 -3
  57. package/dist/functional.cjs +10 -10
  58. package/dist/functional.js +3 -3
  59. package/dist/index.cjs +274 -41
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.d.cts +73 -3
  62. package/dist/index.d.ts +73 -3
  63. package/dist/index.js +227 -11
  64. package/dist/index.js.map +1 -1
  65. package/dist/{init-BMiXSJNM.d.cts → init-BC5aN8bh.d.cts} +18 -0
  66. package/dist/{init-ByRbNTRo.d.ts → init-_FG4IbhF.d.ts} +18 -0
  67. package/dist/instrumentation.cjs +9 -9
  68. package/dist/instrumentation.js +2 -2
  69. package/dist/messaging.cjs +7 -7
  70. package/dist/messaging.js +4 -4
  71. package/dist/parse-error.cjs +13 -0
  72. package/dist/parse-error.cjs.map +1 -0
  73. package/dist/parse-error.d.cts +13 -0
  74. package/dist/parse-error.d.ts +13 -0
  75. package/dist/parse-error.js +4 -0
  76. package/dist/parse-error.js.map +1 -0
  77. package/dist/processors.cjs +2 -2
  78. package/dist/processors.d.cts +40 -4
  79. package/dist/processors.d.ts +40 -4
  80. package/dist/processors.js +1 -1
  81. package/dist/semantic-helpers.cjs +8 -8
  82. package/dist/semantic-helpers.js +4 -4
  83. package/dist/webhook.cjs +4 -4
  84. package/dist/webhook.js +3 -3
  85. package/dist/workflow-distributed.cjs +5 -5
  86. package/dist/workflow-distributed.js +3 -3
  87. package/dist/workflow.cjs +8 -8
  88. package/dist/workflow.js +4 -4
  89. package/dist/yaml-config.d.cts +2 -1
  90. package/dist/yaml-config.d.ts +2 -1
  91. package/package.json +55 -33
  92. package/skills/autotel-core/SKILL.md +162 -0
  93. package/skills/autotel-events/SKILL.md +88 -0
  94. package/skills/autotel-frameworks/SKILL.md +97 -0
  95. package/skills/autotel-instrumentation/SKILL.md +125 -0
  96. package/skills/autotel-request-logging/SKILL.md +100 -0
  97. package/skills/autotel-structured-errors/SKILL.md +130 -0
  98. package/src/drain-pipeline.test.ts +68 -0
  99. package/src/drain-pipeline.ts +199 -0
  100. package/src/enrichers.test.ts +150 -0
  101. package/src/enrichers.ts +145 -0
  102. package/src/flatten-attributes.test.ts +76 -0
  103. package/src/flatten-attributes.ts +80 -0
  104. package/src/functional.test.ts +63 -0
  105. package/src/functional.ts +11 -3
  106. package/src/index.ts +34 -0
  107. package/src/init.ts +22 -0
  108. package/src/package-manifest.test.ts +24 -0
  109. package/src/parse-error.test.ts +73 -0
  110. package/src/parse-error.ts +112 -0
  111. package/src/pretty-log-formatter.test.ts +123 -0
  112. package/src/pretty-log-formatter.ts +210 -0
  113. package/src/processors/canonical-log-line-processor.test.ts +81 -25
  114. package/src/processors/canonical-log-line-processor.ts +130 -42
  115. package/src/request-logger.test.ts +137 -0
  116. package/src/request-logger.ts +150 -0
  117. package/src/structured-error.test.ts +106 -0
  118. package/src/structured-error.ts +97 -0
  119. package/dist/chunk-2RQDNGV3.js +0 -126
  120. package/dist/chunk-2RQDNGV3.js.map +0 -1
  121. package/dist/chunk-BS757SL2.js.map +0 -1
  122. package/dist/chunk-GZFH6P5U.js.map +0 -1
  123. package/dist/chunk-ONK2Y22L.cjs +0 -128
  124. package/dist/chunk-ONK2Y22L.cjs.map +0 -1
  125. package/dist/chunk-QKUGUDXJ.cjs.map +0 -1
  126. package/dist/chunk-VQFF2WMP.cjs.map +0 -1
@@ -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
+ });
@@ -0,0 +1,145 @@
1
+ type Headers = Record<string, string | string[] | undefined>;
2
+
3
+ function get(headers: Headers, key: string): string | undefined {
4
+ const lower = key.toLowerCase();
5
+ for (const [k, v] of Object.entries(headers)) {
6
+ if (k.toLowerCase() === lower) {
7
+ return Array.isArray(v) ? v[0] : v;
8
+ }
9
+ }
10
+ return undefined;
11
+ }
12
+
13
+ // --- User Agent ---
14
+
15
+ export interface UserAgentAttributes {
16
+ 'user_agent.raw': string;
17
+ 'user_agent.browser'?: string;
18
+ 'user_agent.os'?: string;
19
+ 'user_agent.device'?: string;
20
+ }
21
+
22
+ const BROWSER_RE = /(Firefox|OPR|Edg|Chrome|Safari|MSIE|Trident)[\s/]?([\d.]*)/;
23
+ const OS_RE =
24
+ /(Windows NT|Mac OS X|Linux|Android|iPhone OS|iPad|CrOS)[\s]?([\d._]*)/;
25
+
26
+ function parseBrowser(ua: string): string | undefined {
27
+ const m = BROWSER_RE.exec(ua);
28
+ if (!m) return undefined;
29
+ const name =
30
+ m[1] === 'OPR'
31
+ ? 'Opera'
32
+ : m[1] === 'Edg'
33
+ ? 'Edge'
34
+ : m[1] === 'Trident'
35
+ ? 'IE'
36
+ : m[1];
37
+ return m[2] ? `${name} ${m[2]}` : name;
38
+ }
39
+
40
+ function parseOS(ua: string): string | undefined {
41
+ const m = OS_RE.exec(ua);
42
+ if (!m) return undefined;
43
+ const name =
44
+ m[1] === 'iPhone OS'
45
+ ? 'iOS'
46
+ : m[1] === 'Windows NT'
47
+ ? 'Windows'
48
+ : m[1] === 'Mac OS X'
49
+ ? 'macOS'
50
+ : m[1];
51
+ const ver = m[2]?.replaceAll('_', '.') || undefined;
52
+ return ver ? `${name} ${ver}` : name;
53
+ }
54
+
55
+ function parseDevice(ua: string): string | undefined {
56
+ if (/Mobi|Android.*Mobile|iPhone/.test(ua)) return 'mobile';
57
+ if (/iPad|Android(?!.*Mobile)|Tablet/.test(ua)) return 'tablet';
58
+ if (/Bot|Crawler|Spider|Lighthouse/i.test(ua)) return 'bot';
59
+ return 'desktop';
60
+ }
61
+
62
+ export function userAgent(headers: Headers): UserAgentAttributes | undefined {
63
+ const raw = get(headers, 'user-agent');
64
+ if (!raw) return undefined;
65
+
66
+ const attrs: UserAgentAttributes = { 'user_agent.raw': raw };
67
+ const browser = parseBrowser(raw);
68
+ if (browser) attrs['user_agent.browser'] = browser;
69
+ const os = parseOS(raw);
70
+ if (os) attrs['user_agent.os'] = os;
71
+ const device = parseDevice(raw);
72
+ if (device) attrs['user_agent.device'] = device;
73
+
74
+ return attrs;
75
+ }
76
+
77
+ // --- Geo ---
78
+
79
+ export interface GeoAttributes {
80
+ 'geo.country'?: string;
81
+ 'geo.region'?: string;
82
+ 'geo.city'?: string;
83
+ 'geo.latitude'?: string;
84
+ 'geo.longitude'?: string;
85
+ }
86
+
87
+ export function geo(headers: Headers): GeoAttributes | undefined {
88
+ const country =
89
+ get(headers, 'x-vercel-ip-country') ?? get(headers, 'cf-ipcountry');
90
+ const region = get(headers, 'x-vercel-ip-country-region');
91
+ const city = get(headers, 'x-vercel-ip-city');
92
+ const latitude = get(headers, 'x-vercel-ip-latitude');
93
+ const longitude = get(headers, 'x-vercel-ip-longitude');
94
+
95
+ if (!country && !region && !city && !latitude && !longitude) return undefined;
96
+
97
+ const attrs: GeoAttributes = {};
98
+ if (country) attrs['geo.country'] = country;
99
+ if (region) attrs['geo.region'] = region;
100
+ if (city) {
101
+ try {
102
+ attrs['geo.city'] = decodeURIComponent(city);
103
+ } catch {
104
+ attrs['geo.city'] = city;
105
+ }
106
+ }
107
+ if (latitude) attrs['geo.latitude'] = latitude;
108
+ if (longitude) attrs['geo.longitude'] = longitude;
109
+
110
+ return attrs;
111
+ }
112
+
113
+ // --- Request Size ---
114
+
115
+ export interface RequestSizeAttributes {
116
+ 'http.request.body.size'?: number;
117
+ 'http.response.body.size'?: number;
118
+ }
119
+
120
+ const DIGITS_RE = /^\d+$/;
121
+
122
+ function parseContentLength(value: string | undefined): number | undefined {
123
+ if (!value || !DIGITS_RE.test(value)) return undefined;
124
+ return Number(value);
125
+ }
126
+
127
+ export function requestSize(
128
+ requestHeaders: Headers,
129
+ responseHeaders?: Headers,
130
+ ): RequestSizeAttributes | undefined {
131
+ const reqLen = get(requestHeaders, 'content-length');
132
+ const resLen = responseHeaders
133
+ ? get(responseHeaders, 'content-length')
134
+ : undefined;
135
+
136
+ if (!reqLen && !resLen) return undefined;
137
+
138
+ const attrs: RequestSizeAttributes = {};
139
+ const reqBytes = parseContentLength(reqLen);
140
+ if (reqBytes !== undefined) attrs['http.request.body.size'] = reqBytes;
141
+ const resBytes = parseContentLength(resLen);
142
+ if (resBytes !== undefined) attrs['http.response.body.size'] = resBytes;
143
+
144
+ return Object.keys(attrs).length > 0 ? attrs : undefined;
145
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { toAttributeValue, flattenToAttributes } from './flatten-attributes';
3
+
4
+ describe('toAttributeValue', () => {
5
+ it('returns primitives as-is', () => {
6
+ expect(toAttributeValue('hello')).toBe('hello');
7
+ expect(toAttributeValue(42)).toBe(42);
8
+ expect(toAttributeValue(true)).toBe(true);
9
+ });
10
+
11
+ it('returns homogeneous arrays as-is', () => {
12
+ expect(toAttributeValue(['a', 'b'])).toEqual(['a', 'b']);
13
+ expect(toAttributeValue([1, 2])).toEqual([1, 2]);
14
+ expect(toAttributeValue([true, false])).toEqual([true, false]);
15
+ });
16
+
17
+ it('serialises mixed arrays to JSON', () => {
18
+ expect(toAttributeValue([1, 'a'])).toBe('[1,"a"]');
19
+ });
20
+
21
+ it('converts Date to ISO string', () => {
22
+ const d = new Date('2025-01-01T00:00:00Z');
23
+ expect(toAttributeValue(d)).toBe('2025-01-01T00:00:00.000Z');
24
+ });
25
+
26
+ it('converts Error to its message', () => {
27
+ expect(toAttributeValue(new Error('boom'))).toBe('boom');
28
+ });
29
+
30
+ it('returns undefined for plain objects', () => {
31
+ expect(toAttributeValue({ a: 1 })).toBeUndefined();
32
+ });
33
+ });
34
+
35
+ describe('flattenToAttributes', () => {
36
+ it('flattens nested objects with dot-notation keys', () => {
37
+ expect(
38
+ flattenToAttributes({ user: { id: 'u1', plan: 'pro' }, count: 3 }),
39
+ ).toEqual({
40
+ 'user.id': 'u1',
41
+ 'user.plan': 'pro',
42
+ count: 3,
43
+ });
44
+ });
45
+
46
+ it('uses prefix when provided', () => {
47
+ expect(flattenToAttributes({ key: 'val' }, 'error.details')).toEqual({
48
+ 'error.details.key': 'val',
49
+ });
50
+ });
51
+
52
+ it('skips null and undefined values', () => {
53
+ expect(
54
+ flattenToAttributes({ a: 1, b: null, c: undefined, d: 'ok' }),
55
+ ).toEqual({ a: 1, d: 'ok' });
56
+ });
57
+
58
+ it('handles circular references without stack overflow', () => {
59
+ const obj: Record<string, unknown> = { name: 'root' };
60
+ obj.self = obj;
61
+
62
+ const result = flattenToAttributes(obj);
63
+ expect(result).toEqual({
64
+ name: 'root',
65
+ 'self.name': 'root',
66
+ 'self.self': '<circular-reference>',
67
+ });
68
+ });
69
+
70
+ it('serialises non-plain objects to JSON', () => {
71
+ const result = flattenToAttributes({
72
+ date: new Date('2025-01-01T00:00:00Z'),
73
+ });
74
+ expect(result).toEqual({ date: '2025-01-01T00:00:00.000Z' });
75
+ });
76
+ });
@@ -0,0 +1,80 @@
1
+ import type { AttributeValue } from './trace-context';
2
+
3
+ /**
4
+ * Convert an unknown value to an OTel-compatible AttributeValue.
5
+ * Returns undefined when the value cannot be represented.
6
+ */
7
+ export function toAttributeValue(value: unknown): AttributeValue | undefined {
8
+ if (
9
+ typeof value === 'string' ||
10
+ typeof value === 'number' ||
11
+ typeof value === 'boolean'
12
+ ) {
13
+ return value;
14
+ }
15
+ if (Array.isArray(value)) {
16
+ if (
17
+ value.every((v) => typeof v === 'string') ||
18
+ value.every((v) => typeof v === 'number') ||
19
+ value.every((v) => typeof v === 'boolean')
20
+ ) {
21
+ return value as AttributeValue;
22
+ }
23
+ try {
24
+ return JSON.stringify(value);
25
+ } catch {
26
+ return '<serialization-failed>';
27
+ }
28
+ }
29
+ if (value instanceof Date) {
30
+ return value.toISOString();
31
+ }
32
+ if (value instanceof Error) {
33
+ return value.message;
34
+ }
35
+ return undefined;
36
+ }
37
+
38
+ /**
39
+ * Recursively flatten a nested object into dot-notation OTel attributes.
40
+ * Includes circular reference protection via WeakSet.
41
+ */
42
+ export function flattenToAttributes(
43
+ fields: Record<string, unknown>,
44
+ prefix = '',
45
+ ): Record<string, AttributeValue> {
46
+ const out: Record<string, AttributeValue> = {};
47
+ const seen = new WeakSet<object>();
48
+
49
+ function flatten(obj: Record<string, unknown>, currentPrefix: string): void {
50
+ for (const [key, value] of Object.entries(obj)) {
51
+ if (value == null) continue;
52
+ const nextKey = currentPrefix ? `${currentPrefix}.${key}` : key;
53
+
54
+ const attr = toAttributeValue(value);
55
+ if (attr !== undefined) {
56
+ out[nextKey] = attr;
57
+ continue;
58
+ }
59
+
60
+ if (typeof value === 'object' && value.constructor === Object) {
61
+ if (seen.has(value)) {
62
+ out[nextKey] = '<circular-reference>';
63
+ continue;
64
+ }
65
+ seen.add(value);
66
+ flatten(value as Record<string, unknown>, nextKey);
67
+ continue;
68
+ }
69
+
70
+ try {
71
+ out[nextKey] = JSON.stringify(value);
72
+ } catch {
73
+ out[nextKey] = '<serialization-failed>';
74
+ }
75
+ }
76
+ }
77
+
78
+ flatten(fields, prefix);
79
+ return out;
80
+ }
@@ -173,6 +173,69 @@ describe('Functional API', () => {
173
173
  });
174
174
  });
175
175
 
176
+ describe('zero-arg factory pattern (no ctx parameter)', () => {
177
+ it('should detect zero-arg sync factory and execute inner function', () => {
178
+ const collector = createTraceCollector();
179
+
180
+ const addOne = trace(() => (i: number) => {
181
+ return i + 1;
182
+ });
183
+
184
+ const result = addOne(1);
185
+
186
+ expect(result).toBe(2);
187
+ expect(result).not.toBeInstanceOf(Promise);
188
+
189
+ const spans = collector.getSpans();
190
+ expect(spans).toHaveLength(1);
191
+ });
192
+
193
+ it('should detect zero-arg async factory and execute inner function', async () => {
194
+ const collector = createTraceCollector();
195
+
196
+ const fetchData = trace(() => async (query: string) => {
197
+ return query.toUpperCase();
198
+ });
199
+
200
+ const result = await fetchData('test');
201
+
202
+ expect(result).toBe('TEST');
203
+
204
+ const spans = collector.getSpans();
205
+ expect(spans).toHaveLength(1);
206
+ });
207
+
208
+ it('should work with named zero-arg factory', () => {
209
+ const collector = createTraceCollector();
210
+
211
+ const addOne = trace('addOne', () => (i: number) => {
212
+ return i + 1;
213
+ });
214
+
215
+ const result = addOne(1);
216
+
217
+ expect(result).toBe(2);
218
+
219
+ const spans = collector.getSpans();
220
+ expect(spans).toHaveLength(1);
221
+ expect(spans[0]!.name).toBe('addOne');
222
+ });
223
+
224
+ it('should handle multiple zero-arg factories combined', () => {
225
+ const collector = createTraceCollector();
226
+
227
+ const addOne = trace('addOne', () => (i: number) => i + 1);
228
+ const addTwo = trace('addTwo', () => (i: number) => i + 2);
229
+
230
+ const result = addOne(1) + addTwo(1);
231
+
232
+ expect(result).toBe(5);
233
+
234
+ const spans = collector.getSpans();
235
+ expect(spans).toHaveLength(2);
236
+ });
237
+ });
238
+
176
239
  describe('overload 2: trace(name, fn)', () => {
177
240
  it('should use custom name', async () => {
178
241
  const collector = createTraceCollector();
package/src/functional.ts CHANGED
@@ -168,6 +168,14 @@ function looksLikeTraceFactory(fn: GenericFunction): boolean {
168
168
  }
169
169
 
170
170
  if (fn.length === 0) {
171
+ if (!isAsyncFunction(fn)) {
172
+ try {
173
+ const result = fn();
174
+ return typeof result === 'function';
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
171
179
  return false;
172
180
  }
173
181
 
@@ -2361,14 +2369,14 @@ export function withBaggage<T = unknown>(
2361
2369
  }
2362
2370
  return value;
2363
2371
  },
2364
- (err) => {
2372
+ (error) => {
2365
2373
  // Restore original context before rejecting
2366
2374
  if (previousStored) {
2367
2375
  return ctxStorage.run(previousStored, () => {
2368
- throw err;
2376
+ throw error;
2369
2377
  });
2370
2378
  }
2371
- throw err;
2379
+ throw error;
2372
2380
  },
2373
2381
  );
2374
2382
  }
package/src/index.ts CHANGED
@@ -110,6 +110,40 @@ export {
110
110
  // Graceful shutdown
111
111
  export { flush, shutdown } from './shutdown';
112
112
 
113
+ // Request logger
114
+ export {
115
+ getRequestLogger,
116
+ runWithRequestContext,
117
+ type RequestLogger,
118
+ type RequestLogSnapshot,
119
+ type RequestLoggerOptions,
120
+ } from './request-logger';
121
+
122
+ // Structured errors
123
+ export {
124
+ createStructuredError,
125
+ getStructuredErrorAttributes,
126
+ recordStructuredError,
127
+ type StructuredError,
128
+ type StructuredErrorInput,
129
+ } from './structured-error';
130
+
131
+ // parseError
132
+ export { parseError, type ParsedError } from './parse-error';
133
+
134
+ // Attribute flattening
135
+ export { toAttributeValue, flattenToAttributes } from './flatten-attributes';
136
+
137
+ // Drain pipeline
138
+ export {
139
+ createDrainPipeline,
140
+ type DrainPipelineOptions,
141
+ type PipelineDrainFn,
142
+ } from './drain-pipeline';
143
+
144
+ // Pretty log formatting
145
+ export { formatDuration } from './pretty-log-formatter';
146
+
113
147
  // Re-export sampling strategies
114
148
  export {
115
149
  type Sampler,
package/src/init.ts CHANGED
@@ -976,6 +976,23 @@ export interface AutotelConfig {
976
976
  ) => string;
977
977
  /** Whether to include resource attributes (default: true) */
978
978
  includeResourceAttributes?: boolean;
979
+ /** Predicate to decide whether to emit (runs after event is built). */
980
+ shouldEmit?: CanonicalLogLineOptions['shouldEmit'];
981
+ /**
982
+ * Declarative tail sampling conditions (OR logic).
983
+ * Ignored when `shouldEmit` is provided.
984
+ * @example keep: [{ status: 500 }, { durationMs: 1000 }]
985
+ */
986
+ keep?: CanonicalLogLineOptions['keep'];
987
+ /** Callback invoked after emit for custom fan-out. */
988
+ drain?: CanonicalLogLineOptions['drain'];
989
+ /** Handler for drain failures. */
990
+ onDrainError?: CanonicalLogLineOptions['onDrainError'];
991
+ /**
992
+ * Pretty-print canonical log lines to console.
993
+ * Defaults to true when NODE_ENV is 'development'.
994
+ */
995
+ pretty?: boolean;
979
996
  };
980
997
  }
981
998
 
@@ -1247,6 +1264,11 @@ export function init(cfg: AutotelConfig): void {
1247
1264
  messageFormat: mergedConfig.canonicalLogLines.messageFormat,
1248
1265
  includeResourceAttributes:
1249
1266
  mergedConfig.canonicalLogLines.includeResourceAttributes,
1267
+ shouldEmit: mergedConfig.canonicalLogLines.shouldEmit,
1268
+ keep: mergedConfig.canonicalLogLines.keep,
1269
+ drain: mergedConfig.canonicalLogLines.drain,
1270
+ onDrainError: mergedConfig.canonicalLogLines.onDrainError,
1271
+ pretty: mergedConfig.canonicalLogLines.pretty,
1250
1272
  };
1251
1273
  spanProcessors.push(new CanonicalLogLineProcessor(canonicalOptions));
1252
1274
  }
@@ -0,0 +1,24 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ const here = dirname(fileURLToPath(import.meta.url));
7
+ const packageJsonPath = resolve(here, '../package.json');
8
+
9
+ interface PackageJson {
10
+ bin?: Record<string, string>;
11
+ dependencies?: Record<string, string>;
12
+ }
13
+
14
+ describe('package manifest', () => {
15
+ it('declares runtime deps required by published bin scripts', () => {
16
+ const pkg = JSON.parse(
17
+ readFileSync(packageJsonPath, 'utf8'),
18
+ ) as PackageJson;
19
+
20
+ if (pkg.bin?.intent) {
21
+ expect(pkg.dependencies?.['@tanstack/intent']).toBeDefined();
22
+ }
23
+ });
24
+ });