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.
Files changed (84) hide show
  1. package/bin/intent.js +6 -0
  2. package/dist/auto.cjs +2 -2
  3. package/dist/auto.js +1 -1
  4. package/dist/{chunk-GY4CRZSV.js → chunk-2D74YR4Z.js} +3 -3
  5. package/dist/{chunk-GY4CRZSV.js.map → chunk-2D74YR4Z.js.map} +1 -1
  6. package/dist/{chunk-724XLWR3.js → chunk-47KW5CV7.js} +87 -4
  7. package/dist/chunk-47KW5CV7.js.map +1 -0
  8. package/dist/{chunk-3ZFDJJWZ.cjs → chunk-AXNPD6XW.cjs} +26 -26
  9. package/dist/{chunk-3ZFDJJWZ.cjs.map → chunk-AXNPD6XW.cjs.map} +1 -1
  10. package/dist/{chunk-BSZP4URK.cjs → chunk-CLR5RQW4.cjs} +5 -5
  11. package/dist/{chunk-BSZP4URK.cjs.map → chunk-CLR5RQW4.cjs.map} +1 -1
  12. package/dist/{chunk-AXFWWJF3.cjs → chunk-CSG2D3BZ.cjs} +7 -7
  13. package/dist/{chunk-AXFWWJF3.cjs.map → chunk-CSG2D3BZ.cjs.map} +1 -1
  14. package/dist/{chunk-MSUHW2I4.cjs → chunk-CZR7PJUZ.cjs} +13 -13
  15. package/dist/{chunk-MSUHW2I4.cjs.map → chunk-CZR7PJUZ.cjs.map} +1 -1
  16. package/dist/{chunk-IPKXURBW.js → chunk-GSZJOYFF.js} +3 -3
  17. package/dist/{chunk-IPKXURBW.js.map → chunk-GSZJOYFF.js.map} +1 -1
  18. package/dist/{chunk-WCIIFRGL.js → chunk-IJMW6CTV.js} +3 -3
  19. package/dist/{chunk-WCIIFRGL.js.map → chunk-IJMW6CTV.js.map} +1 -1
  20. package/dist/{chunk-HSEIUH7F.cjs → chunk-PDRIJURL.cjs} +86 -2
  21. package/dist/chunk-PDRIJURL.cjs.map +1 -0
  22. package/dist/{chunk-4RZ4JUBY.cjs → chunk-W6U2BUHU.cjs} +5 -5
  23. package/dist/{chunk-4RZ4JUBY.cjs.map → chunk-W6U2BUHU.cjs.map} +1 -1
  24. package/dist/{chunk-5XUEHX7J.js → chunk-Z3BQYTZE.js} +3 -3
  25. package/dist/{chunk-5XUEHX7J.js.map → chunk-Z3BQYTZE.js.map} +1 -1
  26. package/dist/{chunk-3AMR5XLZ.js → chunk-ZJTBAUXG.js} +3 -3
  27. package/dist/{chunk-3AMR5XLZ.js.map → chunk-ZJTBAUXG.js.map} +1 -1
  28. package/dist/decorators.cjs +2 -2
  29. package/dist/decorators.js +2 -2
  30. package/dist/enrichers.cjs +90 -0
  31. package/dist/enrichers.cjs.map +1 -0
  32. package/dist/enrichers.d.cts +23 -0
  33. package/dist/enrichers.d.ts +23 -0
  34. package/dist/enrichers.js +86 -0
  35. package/dist/enrichers.js.map +1 -0
  36. package/dist/event.cjs +5 -5
  37. package/dist/event.js +2 -2
  38. package/dist/functional.cjs +9 -9
  39. package/dist/functional.js +2 -2
  40. package/dist/index.cjs +63 -41
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +10 -3
  43. package/dist/index.d.ts +10 -3
  44. package/dist/index.js +28 -11
  45. package/dist/index.js.map +1 -1
  46. package/dist/{init-BC5aN8bh.d.cts → init-Co9CWOY6.d.cts} +16 -0
  47. package/dist/{init-_FG4IbhF.d.ts → init-oLqNJTpj.d.ts} +16 -0
  48. package/dist/instrumentation.cjs +8 -8
  49. package/dist/instrumentation.js +1 -1
  50. package/dist/messaging.cjs +6 -6
  51. package/dist/messaging.js +3 -3
  52. package/dist/semantic-helpers.cjs +7 -7
  53. package/dist/semantic-helpers.js +3 -3
  54. package/dist/webhook.cjs +3 -3
  55. package/dist/webhook.js +2 -2
  56. package/dist/workflow-distributed.cjs +4 -4
  57. package/dist/workflow-distributed.js +2 -2
  58. package/dist/workflow.cjs +7 -7
  59. package/dist/workflow.js +3 -3
  60. package/dist/yaml-config.d.cts +1 -1
  61. package/dist/yaml-config.d.ts +1 -1
  62. package/package.json +45 -33
  63. package/skills/autotel-core/SKILL.md +162 -0
  64. package/skills/autotel-events/SKILL.md +88 -0
  65. package/skills/autotel-frameworks/SKILL.md +97 -0
  66. package/skills/autotel-instrumentation/SKILL.md +125 -0
  67. package/skills/autotel-request-logging/SKILL.md +100 -0
  68. package/skills/autotel-structured-errors/SKILL.md +130 -0
  69. package/src/enrichers.test.ts +150 -0
  70. package/src/enrichers.ts +145 -0
  71. package/src/index.ts +4 -0
  72. package/src/init-redactor.test.ts +8 -0
  73. package/src/init.ts +56 -0
  74. package/src/package-manifest.test.ts +24 -0
  75. package/src/posthog-logs.test.ts +115 -0
  76. package/src/posthog-logs.ts +77 -0
  77. package/src/redact-values.test.ts +76 -0
  78. package/src/redact-values.ts +27 -0
  79. package/src/request-logger.test.ts +14 -1
  80. package/src/request-logger.ts +11 -1
  81. package/src/structured-error.test.ts +30 -0
  82. package/src/structured-error.ts +11 -0
  83. package/dist/chunk-724XLWR3.js.map +0 -1
  84. 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
+ });