autotel 4.1.0 → 4.2.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 (253) hide show
  1. package/dist/auto.cjs +5 -3
  2. package/dist/auto.cjs.map +1 -1
  3. package/dist/auto.js +3 -3
  4. package/dist/auto.js.map +1 -1
  5. package/dist/chunk-C_NdSu1c.cjs +34 -0
  6. package/dist/correlation-id.cjs +1 -1
  7. package/dist/correlation-id.d.cts.map +1 -1
  8. package/dist/correlation-id.d.ts.map +1 -1
  9. package/dist/correlation-id.js +1 -1
  10. package/dist/decorators.cjs +1 -1
  11. package/dist/decorators.js +1 -1
  12. package/dist/{event-ByBTV9M2.js → event-531asIM6.js} +4 -4
  13. package/dist/{event-ByBTV9M2.js.map → event-531asIM6.js.map} +1 -1
  14. package/dist/{event-BhHREDJk.cjs → event-CcZYwp50.cjs} +4 -4
  15. package/dist/{event-BhHREDJk.cjs.map → event-CcZYwp50.cjs.map} +1 -1
  16. package/dist/event.cjs +1 -1
  17. package/dist/event.js +1 -1
  18. package/dist/{functional-zpzNLhky.cjs → functional-C8B0Qa7o.cjs} +10 -7
  19. package/dist/functional-C8B0Qa7o.cjs.map +1 -0
  20. package/dist/{functional-DtI0u4vx.js → functional-r-AUIRy_.js} +9 -9
  21. package/dist/functional-r-AUIRy_.js.map +1 -0
  22. package/dist/functional.cjs +1 -1
  23. package/dist/functional.js +1 -1
  24. package/dist/http.cjs +1 -1
  25. package/dist/http.js +1 -1
  26. package/dist/index.cjs +15 -13
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -14
  31. package/dist/index.js.map +1 -1
  32. package/dist/{init-D-jnNMix.js → init-BS2JVkrL.js} +2 -2
  33. package/dist/{init-D-jnNMix.js.map → init-BS2JVkrL.js.map} +1 -1
  34. package/dist/{init-BX7AmFRl.cjs → init-BXiuPK6j.cjs} +3 -3
  35. package/dist/{init-BX7AmFRl.cjs.map → init-BXiuPK6j.cjs.map} +1 -1
  36. package/dist/instrumentation.cjs +2 -2
  37. package/dist/instrumentation.js +2 -2
  38. package/dist/logger.cjs +236 -8
  39. package/dist/logger.cjs.map +1 -0
  40. package/dist/messaging.cjs +1 -1
  41. package/dist/messaging.js +1 -1
  42. package/dist/{node-require-DF5QBX6z.cjs → node-require-CZ_PU448.cjs} +6 -4
  43. package/dist/node-require-CZ_PU448.cjs.map +1 -0
  44. package/dist/{node-require-Db1oDpLj.js → node-require-vROmTeJ8.js} +5 -5
  45. package/dist/node-require-vROmTeJ8.js.map +1 -0
  46. package/dist/{operation-context-C-2hmmtP.js → operation-context-CKBoA4Qy.js} +3 -3
  47. package/dist/operation-context-CKBoA4Qy.js.map +1 -0
  48. package/dist/{operation-context-n4_obUwq.cjs → operation-context-D6LDf4W_.cjs} +3 -1
  49. package/dist/operation-context-D6LDf4W_.cjs.map +1 -0
  50. package/dist/register.cjs +3 -1
  51. package/dist/register.cjs.map +1 -1
  52. package/dist/register.js +2 -2
  53. package/dist/register.js.map +1 -1
  54. package/dist/semantic-helpers.cjs +1 -1
  55. package/dist/semantic-helpers.js +1 -1
  56. package/dist/{stable-hash-Cg5cT34Q.js → stable-hash-ChFBIhNt.js} +3 -3
  57. package/dist/stable-hash-ChFBIhNt.js.map +1 -0
  58. package/dist/{stable-hash-BNTMrmdB.cjs → stable-hash-brKISGf1.cjs} +4 -2
  59. package/dist/stable-hash-brKISGf1.cjs.map +1 -0
  60. package/dist/trace-context-Cijqoi6e.d.cts.map +1 -1
  61. package/dist/trace-context-Cijqoi6e.d.ts.map +1 -1
  62. package/dist/trace-helpers.cjs +1 -1
  63. package/dist/trace-helpers.js +1 -1
  64. package/dist/{track-wc0HafS_.js → track-COUuU48p.js} +5 -5
  65. package/dist/track-COUuU48p.js.map +1 -0
  66. package/dist/{track-D59FfpL0.cjs → track-Cb3Q4QmS.cjs} +4 -2
  67. package/dist/track-Cb3Q4QmS.cjs.map +1 -0
  68. package/dist/validate.cjs +1 -1
  69. package/dist/validate.js +1 -1
  70. package/dist/webhook.cjs +1 -1
  71. package/dist/webhook.js +1 -1
  72. package/dist/workflow-distributed.cjs +1 -1
  73. package/dist/workflow-distributed.js +1 -1
  74. package/dist/workflow.cjs +3 -1
  75. package/dist/workflow.cjs.map +1 -1
  76. package/dist/workflow.d.cts.map +1 -1
  77. package/dist/workflow.d.ts.map +1 -1
  78. package/dist/workflow.js +3 -3
  79. package/dist/workflow.js.map +1 -1
  80. package/dist/yaml-config.cjs +233 -4
  81. package/dist/yaml-config.cjs.map +1 -0
  82. package/dist/yaml-config.d.cts.map +1 -1
  83. package/dist/yaml-config.d.ts.map +1 -1
  84. package/dist/yaml-config.js +8 -7
  85. package/dist/yaml-config.js.map +1 -1
  86. package/package.json +1 -2
  87. package/dist/functional-DtI0u4vx.js.map +0 -1
  88. package/dist/functional-zpzNLhky.cjs.map +0 -1
  89. package/dist/logger-thMPLpOG.cjs +0 -487
  90. package/dist/logger-thMPLpOG.cjs.map +0 -1
  91. package/dist/node-require-DF5QBX6z.cjs.map +0 -1
  92. package/dist/node-require-Db1oDpLj.js.map +0 -1
  93. package/dist/operation-context-C-2hmmtP.js.map +0 -1
  94. package/dist/operation-context-n4_obUwq.cjs.map +0 -1
  95. package/dist/stable-hash-BNTMrmdB.cjs.map +0 -1
  96. package/dist/stable-hash-Cg5cT34Q.js.map +0 -1
  97. package/dist/track-D59FfpL0.cjs.map +0 -1
  98. package/dist/track-wc0HafS_.js.map +0 -1
  99. package/dist/yaml-config-Ck2uB0Dp.cjs +0 -273
  100. package/dist/yaml-config-Ck2uB0Dp.cjs.map +0 -1
  101. package/src/attribute-redacting-processor.test.ts +0 -763
  102. package/src/attribute-redacting-processor.ts +0 -621
  103. package/src/attributes/attachers.ts +0 -161
  104. package/src/attributes/builders.ts +0 -529
  105. package/src/attributes/domains.ts +0 -42
  106. package/src/attributes/index.ts +0 -81
  107. package/src/attributes/registry.ts +0 -323
  108. package/src/attributes/types.ts +0 -211
  109. package/src/attributes/utils.ts +0 -64
  110. package/src/attributes/validators.ts +0 -266
  111. package/src/attributes.test.ts +0 -292
  112. package/src/auto.ts +0 -67
  113. package/src/autotel-logger.test.ts +0 -548
  114. package/src/autotel-logger.ts +0 -364
  115. package/src/baggage-span-processor.test.ts +0 -202
  116. package/src/baggage-span-processor.ts +0 -100
  117. package/src/business-baggage.test.ts +0 -500
  118. package/src/business-baggage.ts +0 -669
  119. package/src/circuit-breaker.test.ts +0 -341
  120. package/src/circuit-breaker.ts +0 -184
  121. package/src/config.test.ts +0 -94
  122. package/src/config.ts +0 -172
  123. package/src/correlated-events.test.ts +0 -151
  124. package/src/correlated-events.ts +0 -47
  125. package/src/correlation-id.test.ts +0 -163
  126. package/src/correlation-id.ts +0 -206
  127. package/src/db.test.ts +0 -252
  128. package/src/db.ts +0 -447
  129. package/src/decorators.test.ts +0 -153
  130. package/src/decorators.ts +0 -188
  131. package/src/define-event.test.ts +0 -41
  132. package/src/define-event.ts +0 -58
  133. package/src/devtools.ts +0 -60
  134. package/src/drain-pipeline.test.ts +0 -68
  135. package/src/drain-pipeline.ts +0 -199
  136. package/src/drain-toolkit.test.ts +0 -113
  137. package/src/drain-toolkit.ts +0 -129
  138. package/src/enricher-toolkit.test.ts +0 -67
  139. package/src/enricher-toolkit.ts +0 -79
  140. package/src/enrichers.test.ts +0 -150
  141. package/src/enrichers.ts +0 -145
  142. package/src/env-config.test.ts +0 -323
  143. package/src/env-config.ts +0 -309
  144. package/src/error-catalog.test.ts +0 -133
  145. package/src/error-catalog.ts +0 -262
  146. package/src/event-queue.test.ts +0 -864
  147. package/src/event-queue.ts +0 -699
  148. package/src/event-subscriber.ts +0 -262
  149. package/src/event-testing.ts +0 -197
  150. package/src/event.test.ts +0 -1104
  151. package/src/event.ts +0 -988
  152. package/src/events-config.ts +0 -235
  153. package/src/exporters.ts +0 -165
  154. package/src/filtering-span-processor.test.ts +0 -281
  155. package/src/filtering-span-processor.ts +0 -111
  156. package/src/flatten-attributes.test.ts +0 -76
  157. package/src/flatten-attributes.ts +0 -80
  158. package/src/functional.strict-types.typecheck.ts +0 -53
  159. package/src/functional.test.ts +0 -1464
  160. package/src/functional.ts +0 -2539
  161. package/src/functional.types.test.ts +0 -135
  162. package/src/hook.mjs +0 -15
  163. package/src/http.test.ts +0 -485
  164. package/src/http.ts +0 -424
  165. package/src/index.ts +0 -433
  166. package/src/init-auto-redactor.test.ts +0 -53
  167. package/src/init-redactor.test.ts +0 -8
  168. package/src/init.customization.test.ts +0 -665
  169. package/src/init.integrations.test.ts +0 -399
  170. package/src/init.openllmetry.test.ts +0 -194
  171. package/src/init.protocol.test.ts +0 -215
  172. package/src/init.ts +0 -2439
  173. package/src/instrumentation.test.ts +0 -108
  174. package/src/instrumentation.ts +0 -319
  175. package/src/logger.test.ts +0 -125
  176. package/src/logger.ts +0 -341
  177. package/src/messaging-adapters.test.ts +0 -595
  178. package/src/messaging-adapters.ts +0 -583
  179. package/src/messaging-testing.test.ts +0 -573
  180. package/src/messaging-testing.ts +0 -935
  181. package/src/messaging.test.ts +0 -1646
  182. package/src/messaging.ts +0 -2245
  183. package/src/metric-helpers.ts +0 -47
  184. package/src/metric-testing.ts +0 -197
  185. package/src/metric.ts +0 -446
  186. package/src/metrics.test.ts +0 -241
  187. package/src/node-require.ts +0 -123
  188. package/src/operation-context.ts +0 -93
  189. package/src/parse-error.test.ts +0 -73
  190. package/src/parse-error.ts +0 -112
  191. package/src/posthog-logs.test.ts +0 -115
  192. package/src/posthog-logs.ts +0 -77
  193. package/src/pretty-console-exporter.test.ts +0 -545
  194. package/src/pretty-console-exporter.ts +0 -413
  195. package/src/pretty-log-formatter.test.ts +0 -123
  196. package/src/pretty-log-formatter.ts +0 -210
  197. package/src/processors/canonical-log-line-processor.test.ts +0 -523
  198. package/src/processors/canonical-log-line-processor.ts +0 -396
  199. package/src/processors.ts +0 -152
  200. package/src/rate-limiter.test.ts +0 -199
  201. package/src/rate-limiter.ts +0 -98
  202. package/src/redact-values.test.ts +0 -90
  203. package/src/redact-values.ts +0 -34
  204. package/src/register.ts +0 -37
  205. package/src/request-logger.test.ts +0 -545
  206. package/src/request-logger.ts +0 -342
  207. package/src/sampling.test.ts +0 -1060
  208. package/src/sampling.ts +0 -737
  209. package/src/security-schema.test.ts +0 -45
  210. package/src/security-schema.ts +0 -107
  211. package/src/semantic-conventions.ts +0 -15
  212. package/src/semantic-helpers.test.ts +0 -226
  213. package/src/semantic-helpers.ts +0 -438
  214. package/src/shutdown.test.ts +0 -364
  215. package/src/shutdown.ts +0 -246
  216. package/src/span-name-normalizer.test.ts +0 -377
  217. package/src/span-name-normalizer.ts +0 -213
  218. package/src/stable-hash.ts +0 -27
  219. package/src/structured-error.test.ts +0 -191
  220. package/src/structured-error.ts +0 -157
  221. package/src/stub.integration.test.ts +0 -361
  222. package/src/tail-sampling-processor.test.ts +0 -230
  223. package/src/tail-sampling-processor.ts +0 -55
  224. package/src/test-span-collector.test.ts +0 -234
  225. package/src/test-span-collector.ts +0 -150
  226. package/src/testing.ts +0 -705
  227. package/src/trace-context.test.ts +0 -73
  228. package/src/trace-context.ts +0 -567
  229. package/src/trace-helpers.new.test.ts +0 -278
  230. package/src/trace-helpers.test.ts +0 -290
  231. package/src/trace-helpers.ts +0 -710
  232. package/src/trace-hybrid.test.ts +0 -42
  233. package/src/trace-hybrid.ts +0 -37
  234. package/src/tracer-provider.test.ts +0 -183
  235. package/src/tracer-provider.ts +0 -266
  236. package/src/track.test.ts +0 -154
  237. package/src/track.ts +0 -216
  238. package/src/validate.test.ts +0 -287
  239. package/src/validate.ts +0 -307
  240. package/src/validation-attributes.ts +0 -43
  241. package/src/validation.test.ts +0 -330
  242. package/src/validation.ts +0 -246
  243. package/src/variable-name-inference.test.ts +0 -178
  244. package/src/variable-name-inference.ts +0 -242
  245. package/src/webhook.test.ts +0 -649
  246. package/src/webhook.ts +0 -637
  247. package/src/workflow-distributed.test.ts +0 -786
  248. package/src/workflow-distributed.ts +0 -916
  249. package/src/workflow.async-safety.integration.test.ts +0 -345
  250. package/src/workflow.test.ts +0 -647
  251. package/src/workflow.ts +0 -810
  252. package/src/yaml-config.test.ts +0 -373
  253. package/src/yaml-config.ts +0 -351
@@ -1,113 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { defineDrain, defineHttpDrain } from './drain-toolkit';
3
-
4
- describe('defineDrain', () => {
5
- it('calls send with transformed payloads', async () => {
6
- const send = vi.fn(async () => {});
7
- const drain = defineDrain<
8
- { event: { id: string } },
9
- { key: string },
10
- string
11
- >({
12
- name: 'test',
13
- resolve: async () => ({ key: 'k' }),
14
- transform: (contexts) => contexts.map((c) => c.event.id),
15
- send,
16
- });
17
-
18
- await drain({ event: { id: 'a' } });
19
- expect(send).toHaveBeenCalledWith(['a'], { key: 'k' });
20
- });
21
-
22
- it('skips send when resolve returns null', async () => {
23
- const send = vi.fn(async () => {});
24
- const drain = defineDrain({
25
- name: 'test',
26
- resolve: async () => null,
27
- send,
28
- });
29
-
30
- await drain({ event: { id: 'a' } });
31
- expect(send).not.toHaveBeenCalled();
32
- });
33
-
34
- it('isolates send errors', async () => {
35
- const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
36
- const drain = defineDrain({
37
- name: 'test',
38
- resolve: async () => ({ ok: true }),
39
- send: async () => {
40
- throw new Error('fail');
41
- },
42
- });
43
-
44
- await expect(drain({ event: { id: 'a' } })).resolves.toBeUndefined();
45
- expect(spy).toHaveBeenCalled();
46
- spy.mockRestore();
47
- });
48
- });
49
-
50
- describe('defineHttpDrain', () => {
51
- const originalFetch = globalThis.fetch;
52
-
53
- beforeEach(() => {
54
- vi.restoreAllMocks();
55
- });
56
-
57
- afterEach(() => {
58
- globalThis.fetch = originalFetch;
59
- });
60
-
61
- it('encodes payload and posts via fetch', async () => {
62
- const fetchMock = vi.fn(async () => new Response(null, { status: 200 }));
63
- globalThis.fetch = fetchMock as unknown as typeof fetch;
64
-
65
- const drain = defineHttpDrain<
66
- { event: { id: string } },
67
- { token: string },
68
- { id: string }
69
- >({
70
- name: 'http-drain',
71
- resolve: async () => ({ token: 't' }),
72
- transform: (contexts) => contexts.map((c) => c.event),
73
- encode: (payloads, config) => ({
74
- url: 'https://example.com/ingest',
75
- headers: {
76
- 'content-type': 'application/json',
77
- authorization: `Bearer ${config.token}`,
78
- },
79
- body: JSON.stringify(payloads),
80
- }),
81
- retries: 1,
82
- timeoutMs: 2000,
83
- });
84
-
85
- await drain({ event: { id: 'evt_1' } });
86
-
87
- expect(fetchMock).toHaveBeenCalledTimes(1);
88
- expect(fetchMock.mock.calls[0]?.[0]).toBe('https://example.com/ingest');
89
- });
90
-
91
- it('retries failed requests', async () => {
92
- const fetchMock = vi
93
- .fn()
94
- .mockRejectedValueOnce(new Error('network'))
95
- .mockResolvedValueOnce(new Response(null, { status: 200 }));
96
- globalThis.fetch = fetchMock as unknown as typeof fetch;
97
-
98
- const drain = defineHttpDrain({
99
- name: 'http-drain',
100
- resolve: async () => ({ ok: true }),
101
- encode: () => ({
102
- url: 'https://example.com/ingest',
103
- headers: { 'content-type': 'application/json' },
104
- body: '[]',
105
- }),
106
- retries: 2,
107
- timeoutMs: 2000,
108
- });
109
-
110
- await drain({ event: { id: 'evt_1' } });
111
- expect(fetchMock).toHaveBeenCalledTimes(2);
112
- });
113
- });
@@ -1,129 +0,0 @@
1
- export interface DrainOptions<TContext, TConfig, TPayload = TContext> {
2
- /** Stable identifier used in error logs. */
3
- name: string;
4
- /** Return null to skip draining (e.g. missing API key in dev). */
5
- resolve: () => TConfig | null | Promise<TConfig | null>;
6
- /** Transform contexts into payloads. Defaults to identity. */
7
- transform?: (contexts: TContext[]) => TPayload[];
8
- /** Transport implementation. */
9
- send: (payloads: TPayload[], config: TConfig) => Promise<void>;
10
- }
11
-
12
- export interface HttpDrainRequest {
13
- url: string;
14
- headers: Record<string, string>;
15
- body: string;
16
- }
17
-
18
- export interface HttpDrainOptions<
19
- TContext,
20
- TConfig,
21
- TPayload = TContext,
22
- > extends Omit<DrainOptions<TContext, TConfig, TPayload>, 'send'> {
23
- encode: (payloads: TPayload[], config: TConfig) => HttpDrainRequest | null;
24
- timeoutMs?: number;
25
- retries?: number;
26
- resolveTimeoutMs?: (config: TConfig) => number | undefined;
27
- resolveRetries?: (config: TConfig) => number | undefined;
28
- }
29
-
30
- const DEFAULT_TIMEOUT_MS = 5000;
31
- const DEFAULT_RETRIES = 2;
32
-
33
- function delay(ms: number): Promise<void> {
34
- return new Promise((resolve) => {
35
- const t = setTimeout(resolve, ms);
36
- t.unref?.();
37
- });
38
- }
39
-
40
- async function postWithRetry(options: {
41
- name: string;
42
- request: HttpDrainRequest;
43
- timeoutMs: number;
44
- retries: number;
45
- }): Promise<void> {
46
- const { name, request, timeoutMs, retries } = options;
47
- const attempts = Math.max(1, retries);
48
- let lastError: unknown;
49
-
50
- for (let attempt = 1; attempt <= attempts; attempt++) {
51
- const controller = new AbortController();
52
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
53
- timeout.unref?.();
54
- try {
55
- const response = await fetch(request.url, {
56
- method: 'POST',
57
- headers: request.headers,
58
- body: request.body,
59
- signal: controller.signal,
60
- });
61
- if (!response.ok) {
62
- throw new Error(
63
- `[autotel/${name}] HTTP ${response.status} draining ${request.url}`,
64
- );
65
- }
66
- return;
67
- } catch (error) {
68
- lastError = error;
69
- if (attempt < attempts) {
70
- await delay(100 * attempt);
71
- }
72
- } finally {
73
- clearTimeout(timeout);
74
- }
75
- }
76
-
77
- throw lastError;
78
- }
79
-
80
- export function defineDrain<TContext, TConfig, TPayload = TContext>(
81
- options: DrainOptions<TContext, TConfig, TPayload>,
82
- ): (ctx: TContext | TContext[]) => Promise<void> {
83
- return async (ctx: TContext | TContext[]) => {
84
- const contexts = Array.isArray(ctx) ? ctx : [ctx];
85
- if (contexts.length === 0) return;
86
-
87
- const config = await options.resolve();
88
- if (!config) return;
89
-
90
- const payloads = options.transform
91
- ? options.transform(contexts)
92
- : (contexts as unknown as TPayload[]);
93
-
94
- if (payloads.length === 0) return;
95
-
96
- try {
97
- await options.send(payloads, config);
98
- } catch (error) {
99
- console.error(`[autotel/${options.name}] drain failed:`, error);
100
- }
101
- };
102
- }
103
-
104
- export function defineHttpDrain<TContext, TConfig, TPayload = TContext>(
105
- options: HttpDrainOptions<TContext, TConfig, TPayload>,
106
- ): (ctx: TContext | TContext[]) => Promise<void> {
107
- return defineDrain<TContext, TConfig, TPayload>({
108
- name: options.name,
109
- resolve: options.resolve,
110
- transform: options.transform,
111
- send: async (payloads, config) => {
112
- const request = options.encode(payloads, config);
113
- if (!request) return;
114
- const timeoutMs =
115
- options.resolveTimeoutMs?.(config) ??
116
- options.timeoutMs ??
117
- DEFAULT_TIMEOUT_MS;
118
- const retries =
119
- options.resolveRetries?.(config) ?? options.retries ?? DEFAULT_RETRIES;
120
-
121
- await postWithRetry({
122
- name: options.name,
123
- request,
124
- timeoutMs,
125
- retries,
126
- });
127
- },
128
- });
129
- }
@@ -1,67 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { defineEnricher } from './enricher-toolkit';
3
-
4
- describe('defineEnricher', () => {
5
- it('merges computed values into existing field by default', () => {
6
- const enricher = defineEnricher({
7
- name: 'tenant-enricher',
8
- field: 'tenant',
9
- compute: () => ({ plan: 'pro' }),
10
- });
11
-
12
- const ctx = {
13
- event: { tenant: { id: 't_1' } },
14
- };
15
-
16
- enricher(ctx);
17
- expect(ctx.event).toEqual({ tenant: { id: 't_1', plan: 'pro' } });
18
- });
19
-
20
- it('overwrites field when overwrite=true', () => {
21
- const enricher = defineEnricher(
22
- {
23
- name: 'tenant-enricher',
24
- field: 'tenant',
25
- compute: () => ({ plan: 'pro' }),
26
- },
27
- { overwrite: true },
28
- );
29
-
30
- const ctx = {
31
- event: { tenant: { id: 't_1' } },
32
- };
33
-
34
- enricher(ctx);
35
- expect(ctx.event).toEqual({ tenant: { plan: 'pro' } });
36
- });
37
-
38
- it('skips enrichment when compute returns undefined', () => {
39
- const enricher = defineEnricher({
40
- name: 'noop-enricher',
41
- field: 'tenant',
42
- compute: () => undefined,
43
- });
44
-
45
- const ctx = { event: { a: 1 } as Record<string, unknown> };
46
- enricher(ctx);
47
- expect(ctx.event).toEqual({ a: 1 });
48
- });
49
-
50
- it('isolates compute errors and logs them', () => {
51
- const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
52
- const enricher = defineEnricher({
53
- name: 'broken-enricher',
54
- field: 'tenant',
55
- compute: () => {
56
- throw new Error('boom');
57
- },
58
- });
59
-
60
- const ctx = { event: {} as Record<string, unknown> };
61
- enricher(ctx);
62
-
63
- expect(ctx.event).toEqual({});
64
- expect(spy).toHaveBeenCalled();
65
- spy.mockRestore();
66
- });
67
- });
@@ -1,79 +0,0 @@
1
- export interface EnrichContext<TEvent extends Record<string, unknown>> {
2
- event: TEvent;
3
- request?: {
4
- method?: string;
5
- path?: string;
6
- requestId?: string;
7
- };
8
- response?: {
9
- status?: number;
10
- };
11
- headers?: Record<string, string>;
12
- }
13
-
14
- export interface EnricherDefinition<
15
- TEvent extends Record<string, unknown>,
16
- TValue extends object,
17
- > {
18
- /** Stable identifier used in error logs. */
19
- name: string;
20
- /** Top-level field to merge computed values into. */
21
- field: keyof TEvent & string;
22
- /** Return undefined to skip enrichment. */
23
- compute: (ctx: EnrichContext<TEvent>) => TValue | undefined;
24
- }
25
-
26
- export interface EnricherOptions {
27
- /** Replace existing field value instead of merge. Default false. */
28
- overwrite?: boolean;
29
- }
30
-
31
- function isPlainObject(value: unknown): value is Record<string, unknown> {
32
- return value !== null && typeof value === 'object' && !Array.isArray(value);
33
- }
34
-
35
- function mergeInto(
36
- target: Record<string, unknown>,
37
- source: Record<string, unknown>,
38
- ): void {
39
- for (const key in source) {
40
- const sourceVal = source[key];
41
- if (sourceVal === undefined) continue;
42
- const targetVal = target[key];
43
- if (isPlainObject(sourceVal) && isPlainObject(targetVal)) {
44
- mergeInto(targetVal, sourceVal);
45
- } else {
46
- target[key] = sourceVal;
47
- }
48
- }
49
- }
50
-
51
- export function defineEnricher<
52
- TEvent extends Record<string, unknown>,
53
- TValue extends object,
54
- >(
55
- def: EnricherDefinition<TEvent, TValue>,
56
- options: EnricherOptions = {},
57
- ): (ctx: EnrichContext<TEvent>) => void {
58
- return (ctx: EnrichContext<TEvent>) => {
59
- let computed: TValue | undefined;
60
- try {
61
- computed = def.compute(ctx);
62
- } catch (error) {
63
- console.error(`[autotel/${def.name}] enrich failed:`, error);
64
- return;
65
- }
66
-
67
- if (!computed) return;
68
-
69
- if (options.overwrite || !isPlainObject(ctx.event[def.field])) {
70
- (ctx.event as Record<string, unknown>)[def.field] = computed;
71
- return;
72
- }
73
-
74
- mergeInto(
75
- ctx.event[def.field] as unknown as Record<string, unknown>,
76
- computed as unknown as Record<string, unknown>,
77
- );
78
- };
79
- }
@@ -1,150 +0,0 @@
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
- });
package/src/enrichers.ts DELETED
@@ -1,145 +0,0 @@
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
- }