@outputai/http 0.2.1-next.af8a069.0 → 0.2.1-next.fd72d95.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 (49) hide show
  1. package/dist/fetch/index.d.ts +22 -0
  2. package/dist/fetch/index.js +45 -0
  3. package/dist/fetch/index.spec.js +206 -0
  4. package/dist/fetch/logger.d.ts +45 -0
  5. package/dist/fetch/logger.js +54 -0
  6. package/dist/fetch/logger.spec.js +182 -0
  7. package/dist/fetch/utils.d.ts +36 -0
  8. package/dist/fetch/utils.js +91 -0
  9. package/dist/fetch/utils.spec.js +253 -0
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.js +4 -26
  12. package/dist/index.spec.js +11 -389
  13. package/package.json +3 -2
  14. package/dist/hooks/assign_request_id.d.ts +0 -9
  15. package/dist/hooks/assign_request_id.js +0 -15
  16. package/dist/hooks/index.d.ts +0 -4
  17. package/dist/hooks/index.js +0 -4
  18. package/dist/hooks/trace_error.d.ts +0 -14
  19. package/dist/hooks/trace_error.js +0 -61
  20. package/dist/hooks/trace_error.spec.js +0 -35
  21. package/dist/hooks/trace_request.d.ts +0 -6
  22. package/dist/hooks/trace_request.js +0 -25
  23. package/dist/hooks/trace_request.spec.js +0 -60
  24. package/dist/hooks/trace_response.d.ts +0 -6
  25. package/dist/hooks/trace_response.js +0 -26
  26. package/dist/hooks/trace_response.spec.js +0 -68
  27. package/dist/index.integration.test.d.ts +0 -1
  28. package/dist/index.integration.test.js +0 -389
  29. package/dist/utils/create_trace_id.d.ts +0 -13
  30. package/dist/utils/create_trace_id.js +0 -20
  31. package/dist/utils/create_trace_id.spec.d.ts +0 -1
  32. package/dist/utils/create_trace_id.spec.js +0 -20
  33. package/dist/utils/index.d.ts +0 -4
  34. package/dist/utils/index.js +0 -4
  35. package/dist/utils/parse_request_body.d.ts +0 -7
  36. package/dist/utils/parse_request_body.js +0 -19
  37. package/dist/utils/parse_request_body.spec.d.ts +0 -1
  38. package/dist/utils/parse_request_body.spec.js +0 -19
  39. package/dist/utils/parse_response_body.d.ts +0 -10
  40. package/dist/utils/parse_response_body.js +0 -14
  41. package/dist/utils/parse_response_body.spec.d.ts +0 -1
  42. package/dist/utils/parse_response_body.spec.js +0 -19
  43. package/dist/utils/redact_headers.d.ts +0 -6
  44. package/dist/utils/redact_headers.js +0 -27
  45. package/dist/utils/redact_headers.spec.d.ts +0 -1
  46. package/dist/utils/redact_headers.spec.js +0 -245
  47. /package/dist/{hooks/trace_error.spec.d.ts → fetch/index.spec.d.ts} +0 -0
  48. /package/dist/{hooks/trace_request.spec.d.ts → fetch/logger.spec.d.ts} +0 -0
  49. /package/dist/{hooks/trace_response.spec.d.ts → fetch/utils.spec.d.ts} +0 -0
@@ -1,391 +1,13 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { httpClient, HTTPError, TimeoutError } from './index.js';
3
- import { Tracing } from '@outputai/core/sdk_activity_integration';
4
- import { config } from './config.js';
5
- vi.mock('@outputai/core/sdk_activity_integration', () => ({
6
- Tracing: {
7
- addEventStart: vi.fn(),
8
- addEventEnd: vi.fn(),
9
- addEventError: vi.fn()
10
- }
11
- }));
12
- vi.mock('./config.js', () => ({
13
- config: {
14
- logVerbose: false
15
- }
16
- }));
17
- // Mock ky at the module level to intercept at the source
18
- vi.mock('ky', () => {
19
- const createMockResponse = () => new Response(JSON.stringify({ success: true }), {
20
- status: 200,
21
- headers: { 'content-type': 'application/json' }
22
- });
23
- // Mock error types that match ky's actual error classes
24
- // IMPORTANT: These must be the same instances exported by the mock
25
- // so that instanceof checks work correctly
26
- class MockHTTPError extends Error {
27
- response;
28
- request;
29
- options;
30
- constructor(response, request, options) {
31
- super(`${response.status} ${response.statusText}`);
32
- this.name = 'HTTPError';
33
- this.response = response;
34
- this.request = request;
35
- this.options = options;
36
- }
37
- }
38
- class MockTimeoutError extends Error {
39
- request;
40
- constructor(request) {
41
- super('Request timed out');
42
- this.name = 'TimeoutError';
43
- this.request = request;
44
- }
45
- }
46
- // Helper to extract URL string from various input types
47
- const getUrlString = (input) => {
48
- if (typeof input === 'string') {
49
- return input;
50
- }
51
- if (input instanceof Request) {
52
- return input.url;
53
- }
54
- return input.toString();
55
- };
56
- // Default mock fetch implementation
57
- const defaultMockFetch = (input, init) => {
58
- const urlStr = getUrlString(input);
59
- const request = input instanceof Request ? input : new Request(urlStr, init);
60
- // Simulate timeout error (bypass hooks, thrown at fetch level)
61
- if (urlStr.includes('/timeout')) {
62
- throw new MockTimeoutError(request);
63
- }
64
- // Simulate network error (bypass hooks, thrown at fetch level)
65
- if (urlStr.includes('/network-error')) {
66
- throw new TypeError('Failed to fetch');
67
- }
68
- // Simulate HTTP 500 error (goes through hooks)
69
- if (urlStr.includes('/500')) {
70
- return Promise.resolve(new Response('Internal Server Error', {
71
- status: 500,
72
- statusText: 'Internal Server Error'
73
- }));
74
- }
75
- // Simulate HTTP 404 error (goes through hooks)
76
- if (urlStr.includes('/404')) {
77
- return Promise.resolve(new Response('Not Found', {
78
- status: 404,
79
- statusText: 'Not Found'
80
- }));
81
- }
82
- return Promise.resolve(createMockResponse());
83
- };
84
- class MockKy {
85
- hooks = {};
86
- options = {};
87
- customFetch;
88
- constructor(options = {}) {
89
- this.hooks = options.hooks || {};
90
- // Use provided fetch or default mock fetch
91
- this.customFetch = options.fetch || defaultMockFetch;
92
- // Store options with fetch function included
93
- this.options = {
94
- ...options,
95
- fetch: this.customFetch
96
- };
97
- }
98
- async runHooks(hookType, ...args) {
99
- const hooks = this.hooks[hookType] || [];
100
- for (const hook of hooks) {
101
- await hook(...args);
102
- }
103
- }
104
- async get(url, options = {}) {
105
- return this.makeRequest('GET', url, options);
106
- }
107
- async post(url, options = {}) {
108
- return this.makeRequest('POST', url, options);
109
- }
110
- async put(url, options = {}) {
111
- return this.makeRequest('PUT', url, options);
112
- }
113
- async patch(url, options = {}) {
114
- return this.makeRequest('PATCH', url, options);
115
- }
116
- async delete(url, options = {}) {
117
- return this.makeRequest('DELETE', url, options);
118
- }
119
- async makeRequest(method, url, options = {}) {
120
- // Construct full URL like ky would
121
- const fullUrl = this.options.prefixUrl ? `${this.options.prefixUrl}/${url}` : `https://example.com/${url}`;
122
- const request = new Request(fullUrl, { method });
123
- // Run beforeRequest hooks
124
- await this.runHooks('beforeRequest', request);
125
- // Use the custom fetch (which may be wrapped by applyFetchErrorTracing)
126
- // Fetch-level errors (timeout, network) bypass hooks entirely and will throw
127
- const response = await this.customFetch(request, { method });
128
- // Check for HTTP errors (non-2xx status codes)
129
- if (!response.ok) {
130
- const httpError = new MockHTTPError(response, request, options);
131
- // Run beforeError hooks for HTTP errors
132
- try {
133
- await this.runHooks('beforeError', httpError);
134
- }
135
- catch (hookErr) {
136
- // Hooks can transform the error
137
- throw hookErr;
138
- }
139
- throw httpError;
140
- }
141
- // Run afterResponse hooks for successful responses
142
- await this.runHooks('afterResponse', request, options, response);
143
- return response;
144
- }
145
- extend(options = {}) {
146
- // Handle function-based options (like applyDefaultOptions returns)
147
- const resolvedOptions = typeof options === 'function' ? options(this.options) : options;
148
- const mergedOptions = { ...this.options, ...resolvedOptions };
149
- const mergedHooks = { ...this.hooks };
150
- if (resolvedOptions.hooks) {
151
- Object.entries(resolvedOptions.hooks).forEach(([hookType, hookArray]) => {
152
- mergedHooks[hookType] = [
153
- ...(this.hooks[hookType] || []),
154
- ...(Array.isArray(hookArray) ? hookArray : [])
155
- ];
156
- });
157
- }
158
- mergedOptions.hooks = mergedHooks;
159
- return new MockKy(mergedOptions);
160
- }
161
- create(options = {}) {
162
- return new MockKy(options);
163
- }
164
- }
165
- return {
166
- default: new MockKy(),
167
- create: (options) => new MockKy(options),
168
- HTTPError: MockHTTPError,
169
- TimeoutError: MockTimeoutError
170
- };
171
- });
172
- const mockedTracing = vi.mocked(Tracing, true);
173
- const mockedConfig = vi.mocked(config);
174
- describe('HTTP Client', () => {
175
- beforeEach(() => {
176
- mockedTracing.addEventStart.mockClear();
177
- mockedTracing.addEventEnd.mockClear();
178
- mockedTracing.addEventError.mockClear();
179
- });
180
- describe('httpClient function', () => {
181
- it('should create an HTTP client with default options', () => {
182
- const client = httpClient();
183
- expect(client).toBeDefined();
184
- expect(typeof client.get).toBe('function');
185
- expect(typeof client.post).toBe('function');
186
- expect(typeof client.put).toBe('function');
187
- expect(typeof client.patch).toBe('function');
188
- expect(typeof client.delete).toBe('function');
189
- });
190
- it('should create an HTTP client with custom options', () => {
191
- const client = httpClient({
192
- prefixUrl: 'https://api.example.com',
193
- timeout: 5000
194
- });
195
- expect(client).toBeDefined();
196
- });
197
- it('should allow method chaining with extend', () => {
198
- const client = httpClient();
199
- const extendedClient = client.extend({
200
- headers: { 'X-Custom': 'test' }
201
- });
202
- expect(extendedClient).toBeDefined();
203
- });
204
- });
205
- describe('HTTP Client Interface', () => {
206
- it('should have all HTTP methods', () => {
207
- const client = httpClient();
208
- expect(typeof client.get).toBe('function');
209
- expect(typeof client.post).toBe('function');
210
- expect(typeof client.put).toBe('function');
211
- expect(typeof client.patch).toBe('function');
212
- expect(typeof client.delete).toBe('function');
213
- });
214
- });
215
- describe('Error Exports', () => {
216
- it('should export HTTPError', () => {
217
- expect(HTTPError).toBeDefined();
218
- expect(typeof HTTPError).toBe('function');
219
- });
220
- it('should export TimeoutError', () => {
221
- expect(TimeoutError).toBeDefined();
222
- expect(typeof TimeoutError).toBe('function');
223
- });
224
- });
225
- describe('Tracing Configuration', () => {
226
- it('should not trace headers or bodies by default', async () => {
227
- mockedConfig.logVerbose = false;
228
- const client = httpClient({
229
- prefixUrl: 'https://api.example.com'
230
- });
231
- await client.get('users/1');
232
- expect(mockedTracing.addEventStart).toHaveBeenCalled();
233
- });
234
- it('should trace headers and bodies when verbose logging is enabled', async () => {
235
- mockedConfig.logVerbose = true;
236
- const client = httpClient({
237
- prefixUrl: 'https://api.example.com'
238
- });
239
- await client.post('users', { json: { name: 'test', email: 'test@example.com' } });
240
- expect(mockedTracing.addEventStart).toHaveBeenCalled();
241
- });
242
- });
243
- describe('Hook Preservation', () => {
244
- it('should preserve original hooks when extending client with custom hooks', async () => {
245
- const customBeforeRequestCalled = vi.fn();
246
- const customAfterResponseCalled = vi.fn();
247
- const customBeforeErrorCalled = vi.fn();
248
- const client = httpClient({
249
- prefixUrl: 'https://api.example.com'
250
- });
251
- const extendedClient = client.extend({
252
- hooks: {
253
- beforeRequest: [
254
- async (request) => {
255
- customBeforeRequestCalled();
256
- return request;
257
- }
258
- ],
259
- afterResponse: [
260
- async (_request, _options, response) => {
261
- customAfterResponseCalled();
262
- return response;
263
- }
264
- ],
265
- beforeError: [
266
- async (error) => {
267
- customBeforeErrorCalled();
268
- return error;
269
- }
270
- ]
271
- }
272
- });
273
- await extendedClient.get('users/1');
274
- expect(customBeforeRequestCalled).toHaveBeenCalled();
275
- expect(customAfterResponseCalled).toHaveBeenCalled();
276
- expect(mockedTracing.addEventStart).toHaveBeenCalled();
277
- expect(mockedTracing.addEventEnd).toHaveBeenCalled();
278
- });
279
- });
280
- describe('Mocking Verification', () => {
281
- it('should use mocked responses and not make real HTTP requests', async () => {
282
- const client = httpClient({
283
- prefixUrl: 'https://api.example.com'
284
- });
285
- // Test GET request
286
- const getResponse = await client.get('users/1');
287
- const getData = await getResponse.json();
288
- expect(getData).toEqual({ success: true });
289
- // Test POST request
290
- const postResponse = await client.post('users', { json: { name: 'test' } });
291
- const postData = await postResponse.json();
292
- expect(postData).toEqual({ success: true });
293
- // Verify no actual network delay (should be very fast)
294
- const startTime = Date.now();
295
- await client.get('test');
296
- const duration = Date.now() - startTime;
297
- expect(duration).toBeLessThan(50); // Mocked calls should be nearly instantaneous
298
- // Verify that responses come from mocks, not real HTTP
299
- expect(getData.success).toBe(true);
300
- expect(postData.success).toBe(true);
301
- });
302
- });
303
- describe('Error Tracing', () => {
304
- describe('Fetch-Level Errors (should be traced by wrapped fetch)', () => {
305
- it('should trace timeout errors that bypass ky hooks', async () => {
306
- const client = httpClient({
307
- prefixUrl: 'https://api.example.com'
308
- });
309
- await expect(client.get('users/timeout')).rejects.toThrow(TimeoutError);
310
- // Timeout errors should be traced by the wrapped fetch
311
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
312
- const errorCall = mockedTracing.addEventError.mock.calls[0][0];
313
- expect(errorCall).toHaveProperty('id');
314
- expect(errorCall).toHaveProperty('details');
315
- expect(errorCall.details).toHaveProperty('message');
316
- expect(errorCall.details).toHaveProperty('error');
317
- });
318
- it('should trace network errors that bypass ky hooks', async () => {
319
- const client = httpClient({
320
- prefixUrl: 'https://api.example.com'
321
- });
322
- await expect(client.get('users/network-error')).rejects.toThrow(TypeError);
323
- // Network errors should be traced by the wrapped fetch
324
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
325
- const errorCall = mockedTracing.addEventError.mock.calls[0][0];
326
- expect(errorCall).toHaveProperty('id');
327
- expect(errorCall).toHaveProperty('details');
328
- expect(errorCall.details).toHaveProperty('message');
329
- expect(errorCall.details.message).toBe('Unknown error occurred');
330
- expect(errorCall.details).toHaveProperty('error');
331
- });
332
- });
333
- describe('HTTP Errors (should be traced by beforeError hook only)', () => {
334
- it('should trace HTTP 500 errors via beforeError hook, not fetch wrapper', async () => {
335
- const client = httpClient({
336
- prefixUrl: 'https://api.example.com'
337
- });
338
- await expect(client.get('users/500')).rejects.toThrow(HTTPError);
339
- // HTTP errors should be traced by the beforeError hook (traceError)
340
- // The wrapped fetch should NOT trace it (to prevent double-tracing)
341
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
342
- const errorCall = mockedTracing.addEventError.mock.calls[0][0];
343
- expect(errorCall).toHaveProperty('id');
344
- expect(errorCall).toHaveProperty('details');
345
- expect(errorCall.details).toHaveProperty('status', 500);
346
- expect(errorCall.details).toHaveProperty('statusText', 'Internal Server Error');
347
- });
348
- it('should trace HTTP 404 errors via beforeError hook, not fetch wrapper', async () => {
349
- const client = httpClient({
350
- prefixUrl: 'https://api.example.com'
351
- });
352
- await expect(client.get('users/404')).rejects.toThrow(HTTPError);
353
- // HTTP errors should be traced by the beforeError hook (traceError)
354
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
355
- const errorCall = mockedTracing.addEventError.mock.calls[0][0];
356
- expect(errorCall).toHaveProperty('id');
357
- expect(errorCall).toHaveProperty('details');
358
- expect(errorCall.details).toHaveProperty('status', 404);
359
- expect(errorCall.details).toHaveProperty('statusText', 'Not Found');
360
- });
361
- it('should not double-trace HTTP errors', async () => {
362
- const client = httpClient({
363
- prefixUrl: 'https://api.example.com'
364
- });
365
- await expect(client.get('users/500')).rejects.toThrow(HTTPError);
366
- // Should only be traced once (by beforeError hook)
367
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
368
- });
369
- });
370
- describe('Error Type Differentiation', () => {
371
- it('should handle timeout and HTTP errors differently in the same client', async () => {
372
- const client = httpClient({
373
- prefixUrl: 'https://api.example.com'
374
- });
375
- // Test timeout error
376
- mockedTracing.addEventError.mockClear();
377
- await expect(client.get('users/timeout')).rejects.toThrow(TimeoutError);
378
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
379
- const timeoutCall = mockedTracing.addEventError.mock.calls[0][0];
380
- expect(timeoutCall.details).toHaveProperty('message');
381
- expect(timeoutCall.details).toHaveProperty('error');
382
- // Test HTTP error
383
- mockedTracing.addEventError.mockClear();
384
- await expect(client.get('users/500')).rejects.toThrow(HTTPError);
385
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
386
- const httpCall = mockedTracing.addEventError.mock.calls[0][0];
387
- expect(httpCall.details).toHaveProperty('status', 500);
388
- });
389
- });
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { httpClient } from './index.js';
3
+ describe('httpClient', () => {
4
+ it('passes the injected fetch to ky so requests use the custom implementation', async () => {
5
+ const spyFetch = vi.fn((_input, _init) => Promise.resolve(new Response(JSON.stringify({ ok: true }), { status: 200 })));
6
+ const client = httpClient({
7
+ fetch: spyFetch,
8
+ prefixUrl: 'https://example.com'
9
+ });
10
+ await client.get('path');
11
+ expect(spyFetch).toHaveBeenCalled();
390
12
  });
391
13
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/http",
3
- "version": "0.2.1-next.af8a069.0",
3
+ "version": "0.2.1-next.fd72d95.0",
4
4
  "description": "Framework abstraction to make HTTP calls with tracing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,8 @@
10
10
  ],
11
11
  "dependencies": {
12
12
  "ky": "1.14.3",
13
- "@outputai/core": "0.2.1-next.af8a069.0"
13
+ "undici": "8.1.0",
14
+ "@outputai/core": "0.2.1-next.fd72d95.0"
14
15
  },
15
16
  "license": "Apache-2.0",
16
17
  "publishConfig": {
@@ -1,9 +0,0 @@
1
- import type { BeforeRequestHook } from 'ky';
2
- /**
3
- * Assigns a unique request ID to each request via X-Request-ID header
4
- * This ensures each request invocation has a unique identifier for tracing,
5
- * even if the request shape (method/url/headers) is identical
6
- *
7
- * If X-Request-ID already exists (from upstream), it's preserved for propagation
8
- */
9
- export declare const assignRequestId: BeforeRequestHook;
@@ -1,15 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- /**
3
- * Assigns a unique request ID to each request via X-Request-ID header
4
- * This ensures each request invocation has a unique identifier for tracing,
5
- * even if the request shape (method/url/headers) is identical
6
- *
7
- * If X-Request-ID already exists (from upstream), it's preserved for propagation
8
- */
9
- export const assignRequestId = (request) => {
10
- const existingId = request.headers.get('X-Request-ID');
11
- if (!existingId) {
12
- const requestId = randomUUID();
13
- request.headers.set('X-Request-ID', requestId);
14
- }
15
- };
@@ -1,4 +0,0 @@
1
- export { assignRequestId } from './assign_request_id.js';
2
- export { traceRequest } from './trace_request.js';
3
- export { traceResponse } from './trace_response.js';
4
- export { traceError } from './trace_error.js';
@@ -1,4 +0,0 @@
1
- export { assignRequestId } from './assign_request_id.js';
2
- export { traceRequest } from './trace_request.js';
3
- export { traceResponse } from './trace_response.js';
4
- export { traceError } from './trace_error.js';
@@ -1,14 +0,0 @@
1
- import type { BeforeErrorHook, Input } from 'ky';
2
- /**
3
- * Wraps a fetch-like function to log and rethrow errors.
4
- *
5
- * This is nessesary as ky's beforeError hook does not trace non-HTTP errors.
6
- * See: https://github.com/sindresorhus/ky/issues/296
7
- * @param fetchFn - A fetch-compatible function (input, init) => Promise<Response>
8
- * @returns A new function with the same signature that logs and rethrows errors.
9
- */
10
- export declare function applyFetchErrorTracing(fetchFn: (input: Input, init?: RequestInit) => Promise<Response>): (input: Input, init?: RequestInit) => Promise<Response>;
11
- /**
12
- * Traces HTTP errors for observability using Output.ai tracing
13
- */
14
- export declare const traceError: BeforeErrorHook;
@@ -1,61 +0,0 @@
1
- import { HTTPError } from 'ky';
2
- import { createTraceId, redactHeaders } from '#utils/index.js';
3
- import { Tracing } from '@outputai/core/sdk_activity_integration';
4
- /**
5
- * Wraps a fetch-like function to log and rethrow errors.
6
- *
7
- * This is nessesary as ky's beforeError hook does not trace non-HTTP errors.
8
- * See: https://github.com/sindresorhus/ky/issues/296
9
- * @param fetchFn - A fetch-compatible function (input, init) => Promise<Response>
10
- * @returns A new function with the same signature that logs and rethrows errors.
11
- */
12
- export function applyFetchErrorTracing(fetchFn) {
13
- return async (input, init) => {
14
- try {
15
- return await fetchFn(input, init);
16
- }
17
- catch (err) {
18
- const isHTTPError = err instanceof HTTPError;
19
- if (!isHTTPError) {
20
- const traceId = createTraceId(input);
21
- // Skip tracing if no X-Request-ID header is present
22
- if (traceId) {
23
- const isAbortError = err instanceof DOMException && err.name === 'AbortError';
24
- const message = isAbortError ? 'Fetch aborted due to timeout or manual cancellation' : 'Unknown error occurred';
25
- Tracing.addEventError({
26
- id: traceId,
27
- details: {
28
- error: JSON.stringify(err, null, 2),
29
- message
30
- }
31
- });
32
- }
33
- else {
34
- console.warn('applyFetchErrorTracing: Skipping fetch error tracing - no X-Request-ID header');
35
- }
36
- }
37
- throw err;
38
- }
39
- };
40
- }
41
- /**
42
- * Traces HTTP errors for observability using Output.ai tracing
43
- */
44
- export const traceError = (error, _state) => {
45
- const traceId = createTraceId(error.request);
46
- // Skip tracing if no X-Request-ID header is present
47
- if (traceId) {
48
- Tracing.addEventError({
49
- id: traceId,
50
- details: {
51
- status: error.response.status,
52
- statusText: error.response.statusText,
53
- headers: redactHeaders(Object.fromEntries(error.response.headers.entries()))
54
- }
55
- });
56
- }
57
- else {
58
- console.warn('traceError: Skipping HTTP error tracing - no X-Request-ID header');
59
- }
60
- return error;
61
- };
@@ -1,35 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { HTTPError } from 'ky';
3
- import { traceError } from './trace_error.js';
4
- import { Tracing } from '@outputai/core/sdk_activity_integration';
5
- vi.mock('../utils/index.js', () => ({
6
- redactHeaders: vi.fn((h) => h),
7
- createTraceId: vi.fn(() => 'trace-id')
8
- }));
9
- vi.mock('@outputai/core/sdk_activity_integration', () => ({
10
- Tracing: {
11
- addEventError: vi.fn()
12
- }
13
- }));
14
- const mockedTracing = vi.mocked(Tracing, true);
15
- describe('http/hooks/trace_error', () => {
16
- beforeEach(() => {
17
- mockedTracing.addEventError.mockClear();
18
- });
19
- it('traces error with response details when response exists', async () => {
20
- const request = new Request('https://api.example.com/users/1', { method: 'GET' });
21
- const response = new Response('Unauthorized', {
22
- status: 401,
23
- statusText: 'Unauthorized',
24
- headers: { authorization: 'secret', 'x-custom': 'v' }
25
- });
26
- const error = new HTTPError(response, request, {});
27
- const returned = await traceError(error, { retryCount: 0 });
28
- expect(returned).toBe(error);
29
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
30
- const arg = mockedTracing.addEventError.mock.calls[0][0];
31
- expect(arg.details.status).toBe(401);
32
- expect(arg.details.statusText).toBe('Unauthorized');
33
- expect(arg.details.headers).toMatchObject({ authorization: 'secret', 'x-custom': 'v' });
34
- });
35
- });
@@ -1,6 +0,0 @@
1
- import type { BeforeRequestHook } from 'ky';
2
- /**
3
- * Traces HTTP request for observability using Output.ai tracing
4
- * Respects OUTPUT_TRACE_HTTP_VERBOSE environment variable for detailed logging
5
- */
6
- export declare const traceRequest: BeforeRequestHook;
@@ -1,25 +0,0 @@
1
- import { Tracing } from '@outputai/core/sdk_activity_integration';
2
- import { redactHeaders, createTraceId, parseRequestBody } from '#utils/index.js';
3
- import { config } from '#config.js';
4
- /**
5
- * Traces HTTP request for observability using Output.ai tracing
6
- * Respects OUTPUT_TRACE_HTTP_VERBOSE environment variable for detailed logging
7
- */
8
- export const traceRequest = async (request, _options, _state) => {
9
- const traceId = createTraceId(request);
10
- // Skip tracing if no X-Request-ID header is present
11
- if (!traceId) {
12
- console.warn('traceRequest: Skipping request tracing - no X-Request-ID header');
13
- return;
14
- }
15
- const details = {
16
- method: request.method,
17
- url: request.url
18
- };
19
- if (config.logVerbose) {
20
- const headers = Object.fromEntries(request.headers.entries());
21
- details.headers = redactHeaders(headers);
22
- details.body = await parseRequestBody(request);
23
- }
24
- Tracing.addEventStart({ id: traceId, kind: 'http', name: 'request', details });
25
- };
@@ -1,60 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { traceRequest } from './trace_request.js';
3
- import { Tracing } from '@outputai/core/sdk_activity_integration';
4
- import { config } from '../config.js';
5
- vi.mock('../utils/index.js', () => ({
6
- redactHeaders: vi.fn((h) => h),
7
- parseRequestBody: vi.fn(async () => ({ mocked: true })),
8
- createTraceId: vi.fn(() => 'trace-id')
9
- }));
10
- vi.mock('@outputai/core/sdk_activity_integration', () => ({
11
- Tracing: {
12
- addEventStart: vi.fn()
13
- }
14
- }));
15
- vi.mock('../config.js', () => ({
16
- config: { logVerbose: false }
17
- }));
18
- const mockedTracing = vi.mocked(Tracing, true);
19
- const mockedConfig = vi.mocked(config);
20
- describe('http/hooks/trace_request', () => {
21
- beforeEach(() => {
22
- mockedTracing.addEventStart.mockClear();
23
- mockedConfig.logVerbose = false;
24
- });
25
- it('traces minimal details when verbose logging is disabled', async () => {
26
- mockedConfig.logVerbose = false;
27
- const request = new Request('https://api.example.com/users/1', { method: 'GET' });
28
- const options = {};
29
- const state = { retryCount: 0 };
30
- await traceRequest(request, options, state);
31
- expect(mockedTracing.addEventStart).toHaveBeenCalledTimes(1);
32
- const arg = mockedTracing.addEventStart.mock.calls[0][0];
33
- expect(arg).toHaveProperty('kind', 'http');
34
- expect(arg).toHaveProperty('name', 'request');
35
- expect(arg.details).toEqual({
36
- method: 'GET',
37
- url: 'https://api.example.com/users/1'
38
- });
39
- });
40
- it('traces headers and parsed body when verbose logging is enabled', async () => {
41
- mockedConfig.logVerbose = true;
42
- const request = new Request('https://api.example.com/users', {
43
- method: 'POST',
44
- headers: {
45
- authorization: 'secret',
46
- 'x-custom': 'value'
47
- },
48
- body: JSON.stringify({ name: 'test' })
49
- });
50
- const options = {};
51
- const state = { retryCount: 0 };
52
- await traceRequest(request, options, state);
53
- expect(mockedTracing.addEventStart).toHaveBeenCalledTimes(1);
54
- const arg = mockedTracing.addEventStart.mock.calls[0][0];
55
- expect(arg.details.method).toBe('POST');
56
- expect(arg.details.url).toBe('https://api.example.com/users');
57
- expect(arg.details.headers).toMatchObject({ authorization: 'secret', 'x-custom': 'value' });
58
- expect(arg.details.body).toEqual({ mocked: true });
59
- });
60
- });