@outputai/http 0.2.1-next.af8a069.0 → 0.2.1-next.f1502fb.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.
- package/dist/fetch/index.d.ts +22 -0
- package/dist/fetch/index.js +45 -0
- package/dist/fetch/index.spec.js +206 -0
- package/dist/fetch/logger.d.ts +45 -0
- package/dist/fetch/logger.js +54 -0
- package/dist/fetch/logger.spec.js +182 -0
- package/dist/fetch/utils.d.ts +36 -0
- package/dist/fetch/utils.js +91 -0
- package/dist/fetch/utils.spec.js +253 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -26
- package/dist/index.spec.js +11 -389
- package/package.json +3 -2
- package/dist/hooks/assign_request_id.d.ts +0 -9
- package/dist/hooks/assign_request_id.js +0 -15
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -4
- package/dist/hooks/trace_error.d.ts +0 -14
- package/dist/hooks/trace_error.js +0 -61
- package/dist/hooks/trace_error.spec.js +0 -35
- package/dist/hooks/trace_request.d.ts +0 -6
- package/dist/hooks/trace_request.js +0 -25
- package/dist/hooks/trace_request.spec.js +0 -60
- package/dist/hooks/trace_response.d.ts +0 -6
- package/dist/hooks/trace_response.js +0 -26
- package/dist/hooks/trace_response.spec.js +0 -68
- package/dist/index.integration.test.d.ts +0 -1
- package/dist/index.integration.test.js +0 -389
- package/dist/utils/create_trace_id.d.ts +0 -13
- package/dist/utils/create_trace_id.js +0 -20
- package/dist/utils/create_trace_id.spec.d.ts +0 -1
- package/dist/utils/create_trace_id.spec.js +0 -20
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.js +0 -4
- package/dist/utils/parse_request_body.d.ts +0 -7
- package/dist/utils/parse_request_body.js +0 -19
- package/dist/utils/parse_request_body.spec.d.ts +0 -1
- package/dist/utils/parse_request_body.spec.js +0 -19
- package/dist/utils/parse_response_body.d.ts +0 -10
- package/dist/utils/parse_response_body.js +0 -14
- package/dist/utils/parse_response_body.spec.d.ts +0 -1
- package/dist/utils/parse_response_body.spec.js +0 -19
- package/dist/utils/redact_headers.d.ts +0 -6
- package/dist/utils/redact_headers.js +0 -27
- package/dist/utils/redact_headers.spec.d.ts +0 -1
- package/dist/utils/redact_headers.spec.js +0 -245
- /package/dist/{hooks/trace_error.spec.d.ts → fetch/index.spec.d.ts} +0 -0
- /package/dist/{hooks/trace_request.spec.d.ts → fetch/logger.spec.d.ts} +0 -0
- /package/dist/{hooks/trace_response.spec.d.ts → fetch/utils.spec.d.ts} +0 -0
package/dist/index.spec.js
CHANGED
|
@@ -1,391 +1,13 @@
|
|
|
1
|
-
import { describe, it, expect,
|
|
2
|
-
import { httpClient
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
vi.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.1-next.f1502fb.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
|
-
"
|
|
13
|
+
"undici": "8.1.0",
|
|
14
|
+
"@outputai/core": "0.2.1-next.f1502fb.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
|
-
};
|
package/dist/hooks/index.d.ts
DELETED
package/dist/hooks/index.js
DELETED
|
@@ -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,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
|
-
});
|