@outputai/http 0.1.2-dev.0 → 0.1.2

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 (43) hide show
  1. package/package.json +2 -2
  2. package/dist/config.d.ts +0 -10
  3. package/dist/config.js +0 -10
  4. package/dist/hooks/assign_request_id.d.ts +0 -9
  5. package/dist/hooks/assign_request_id.js +0 -15
  6. package/dist/hooks/index.d.ts +0 -4
  7. package/dist/hooks/index.js +0 -4
  8. package/dist/hooks/trace_error.d.ts +0 -14
  9. package/dist/hooks/trace_error.js +0 -61
  10. package/dist/hooks/trace_error.spec.d.ts +0 -1
  11. package/dist/hooks/trace_error.spec.js +0 -35
  12. package/dist/hooks/trace_request.d.ts +0 -6
  13. package/dist/hooks/trace_request.js +0 -25
  14. package/dist/hooks/trace_request.spec.d.ts +0 -1
  15. package/dist/hooks/trace_request.spec.js +0 -60
  16. package/dist/hooks/trace_response.d.ts +0 -6
  17. package/dist/hooks/trace_response.js +0 -26
  18. package/dist/hooks/trace_response.spec.d.ts +0 -1
  19. package/dist/hooks/trace_response.spec.js +0 -68
  20. package/dist/index.d.ts +0 -26
  21. package/dist/index.integration.test.d.ts +0 -1
  22. package/dist/index.integration.test.js +0 -389
  23. package/dist/index.js +0 -51
  24. package/dist/index.spec.d.ts +0 -1
  25. package/dist/index.spec.js +0 -391
  26. package/dist/utils/create_trace_id.d.ts +0 -13
  27. package/dist/utils/create_trace_id.js +0 -20
  28. package/dist/utils/create_trace_id.spec.d.ts +0 -1
  29. package/dist/utils/create_trace_id.spec.js +0 -20
  30. package/dist/utils/index.d.ts +0 -4
  31. package/dist/utils/index.js +0 -4
  32. package/dist/utils/parse_request_body.d.ts +0 -7
  33. package/dist/utils/parse_request_body.js +0 -19
  34. package/dist/utils/parse_request_body.spec.d.ts +0 -1
  35. package/dist/utils/parse_request_body.spec.js +0 -19
  36. package/dist/utils/parse_response_body.d.ts +0 -10
  37. package/dist/utils/parse_response_body.js +0 -14
  38. package/dist/utils/parse_response_body.spec.d.ts +0 -1
  39. package/dist/utils/parse_response_body.spec.js +0 -19
  40. package/dist/utils/redact_headers.d.ts +0 -6
  41. package/dist/utils/redact_headers.js +0 -27
  42. package/dist/utils/redact_headers.spec.d.ts +0 -1
  43. package/dist/utils/redact_headers.spec.js +0 -245
@@ -1,389 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import ky from 'ky';
3
- import { httpClient } from './index.js';
4
- import { Tracing } from '@outputai/core/sdk_activity_integration';
5
- import createTraceId from './utils/create_trace_id.js';
6
- import { traceRequest, traceResponse, traceError } from './hooks/index.js';
7
- import { applyFetchErrorTracing } from './hooks/trace_error.js';
8
- // Helper function for trace ID format validation
9
- const isUuidFormat = (traceId) => {
10
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(traceId);
11
- };
12
- vi.mock('@outputai/core/sdk_activity_integration', () => ({
13
- Tracing: {
14
- addEventStart: vi.fn(),
15
- addEventEnd: vi.fn(),
16
- addEventError: vi.fn()
17
- }
18
- }));
19
- const mockedTracing = vi.mocked(Tracing, true);
20
- // Helper to get header value (httpbingo returns arrays)
21
- const getHeader = (headers, key) => {
22
- const value = headers[key];
23
- return Array.isArray(value) ? value[0] : value;
24
- };
25
- describe('HTTP Client Authentication Integration', () => {
26
- const httpBinClient = httpClient({
27
- prefixUrl: 'https://httpbingo.org',
28
- timeout: 5000
29
- });
30
- const clientsClient = httpBinClient.extend({
31
- headers: {
32
- 'X-API-Key': 'demo-api-key-12345'
33
- }
34
- });
35
- const contractsClient = httpBinClient.extend({
36
- headers: {
37
- Authorization: `Basic ${Buffer.from('demo-user:demo-pass').toString('base64')}`
38
- }
39
- });
40
- describe('Authentication Headers', () => {
41
- it('should include API key for clients endpoints', async () => {
42
- const response = await clientsClient.get('anything/clients');
43
- const data = await response.json();
44
- expect(getHeader(data.headers, 'X-Api-Key')).toBe('demo-api-key-12345');
45
- expect(data.url).toContain('/anything/clients');
46
- expect(data.method).toBe('GET');
47
- }, 10000);
48
- it('should include Basic auth for contracts endpoints', async () => {
49
- const response = await contractsClient.get('anything/contracts');
50
- const data = await response.json();
51
- const authHeader = getHeader(data.headers, 'Authorization');
52
- expect(authHeader).toMatch(/^Basic /);
53
- expect(authHeader).toBe(`Basic ${Buffer.from('demo-user:demo-pass').toString('base64')}`);
54
- expect(data.url).toContain('/anything/contracts');
55
- }, 10000);
56
- it('should remove auth headers when overridden with undefined', async () => {
57
- const response = await clientsClient.get('anything/clients/export', {
58
- headers: { 'X-API-Key': undefined }
59
- });
60
- const data = await response.json();
61
- expect(getHeader(data.headers, 'X-Api-Key')).toBeUndefined();
62
- expect(data.url).toContain('/anything/clients/export');
63
- }, 10000);
64
- it('should properly handle POST with JSON data and authentication', async () => {
65
- const testData = { name: 'Test Client', email: 'test@example.com' };
66
- const response = await clientsClient.post('anything/clients', { json: testData });
67
- const data = await response.json();
68
- expect(getHeader(data.headers, 'X-Api-Key')).toBe('demo-api-key-12345');
69
- expect(data.json).toEqual(testData);
70
- expect(data.method).toBe('POST');
71
- }, 10000);
72
- });
73
- describe('URL Path Construction', () => {
74
- it('should correctly build URLs with chained prefixUrl', async () => {
75
- const response = await clientsClient.get('anything/clients/details');
76
- const data = await response.json();
77
- expect(data.url).toContain('/anything/clients/details');
78
- }, 10000);
79
- it('should handle root path correctly', async () => {
80
- const response = await contractsClient.get('anything/contracts');
81
- const data = await response.json();
82
- expect(data.url).toMatch(/\/anything\/contracts\/?$/);
83
- }, 10000);
84
- it('should handle POST to specific endpoints', async () => {
85
- const testContract = { clientId: '123', title: 'Test Contract', value: 5000 };
86
- const response = await contractsClient.post('anything/contracts/create', { json: testContract });
87
- const data = await response.json();
88
- expect(data.url).toContain('/anything/contracts/create');
89
- expect(data.method).toBe('POST');
90
- expect(data.json).toEqual(testContract);
91
- }, 10000);
92
- });
93
- describe('Authentication Override Patterns', () => {
94
- it('should allow per-request header overrides', async () => {
95
- const response = await clientsClient.get('anything/clients/public', {
96
- headers: {
97
- 'X-API-Key': 'different-key-456'
98
- }
99
- });
100
- const data = await response.json();
101
- expect(getHeader(data.headers, 'X-Api-Key')).toBe('different-key-456');
102
- }, 10000);
103
- it('should support multiple authentication methods', async () => {
104
- const response = await contractsClient.get('anything/contracts/special', {
105
- headers: {
106
- 'X-Special-Token': 'special-value',
107
- 'X-Client-ID': 'client-123'
108
- }
109
- });
110
- const data = await response.json();
111
- expect(getHeader(data.headers, 'Authorization')).toMatch(/^Basic /);
112
- expect(getHeader(data.headers, 'X-Special-Token')).toBe('special-value');
113
- expect(getHeader(data.headers, 'X-Client-Id')).toBe('client-123');
114
- }, 10000);
115
- });
116
- describe('Real API Client Pattern', () => {
117
- it('should demonstrate the complete authentication pattern', async () => {
118
- const clientsResponse = await clientsClient.get('anything/clients');
119
- const clientsData = await clientsResponse.json();
120
- expect(getHeader(clientsData.headers, 'X-Api-Key')).toBe('demo-api-key-12345');
121
- const createResponse = await clientsClient.post('anything/clients', {
122
- json: { name: 'Test Client', email: 'test@example.com' }
123
- });
124
- const createData = await createResponse.json();
125
- expect(getHeader(createData.headers, 'X-Api-Key')).toBe('demo-api-key-12345');
126
- const exportResponse = await clientsClient.get('anything/clients/export', { headers: { 'X-API-Key': undefined } });
127
- const exportData = await exportResponse.json();
128
- expect(getHeader(exportData.headers, 'X-Api-Key')).toBeUndefined();
129
- const contractsResponse = await contractsClient.get('anything/contracts');
130
- const contractsResponseData = await contractsResponse.json();
131
- expect(getHeader(contractsResponseData.headers, 'Authorization')).toMatch(/^Basic /);
132
- const contractCreateResponse = await contractsClient.post('anything/contracts', {
133
- json: { clientId: '123', title: 'Service Agreement', value: 10000 }
134
- });
135
- const contractCreateData = await contractCreateResponse.json();
136
- expect(getHeader(contractCreateData.headers, 'Authorization')).toMatch(/^Basic /);
137
- }, 15000);
138
- });
139
- describe('Error Tracing', () => {
140
- beforeEach(() => {
141
- mockedTracing.addEventStart.mockClear();
142
- mockedTracing.addEventEnd.mockClear();
143
- mockedTracing.addEventError.mockClear();
144
- });
145
- it('should trace timeout errors exactly once (no double-tracing)', async () => {
146
- const timeoutClient = httpClient({
147
- prefixUrl: 'https://httpbingo.org',
148
- timeout: 1 // 1ms timeout will definitely fail on /delay/5
149
- });
150
- // Timeout will throw either TimeoutError or DOMException (AbortError)
151
- await expect(timeoutClient.get('delay/5')).rejects.toThrow();
152
- // Timeout errors should be traced by the wrapped fetch exactly once
153
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
154
- const errorCall = mockedTracing.addEventError.mock.calls[0][0];
155
- expect(errorCall).toHaveProperty('id');
156
- expect(errorCall).toHaveProperty('details');
157
- // In real execution, ky's timeout throws a DOMException (AbortError)
158
- expect(errorCall.details).toHaveProperty('message');
159
- expect(errorCall.details.message).toMatch(/Fetch aborted|Unknown error/);
160
- }, 10000);
161
- });
162
- describe('Trace ID Lifecycle', () => {
163
- beforeEach(() => {
164
- mockedTracing.addEventStart.mockClear();
165
- mockedTracing.addEventEnd.mockClear();
166
- mockedTracing.addEventError.mockClear();
167
- });
168
- it('should use same UUID trace ID for addEventStart and addEventEnd on successful requests', async () => {
169
- const response = await httpBinClient.get('anything/success-trace');
170
- expect(response.status).toBe(200);
171
- expect(mockedTracing.addEventStart).toHaveBeenCalledTimes(1);
172
- expect(mockedTracing.addEventEnd).toHaveBeenCalledTimes(1);
173
- const startCall = mockedTracing.addEventStart.mock.calls[0][0];
174
- const endCall = mockedTracing.addEventEnd.mock.calls[0][0];
175
- expect(isUuidFormat(startCall.id)).toBe(true);
176
- expect(isUuidFormat(endCall.id)).toBe(true);
177
- expect(startCall.id).toBe(endCall.id);
178
- }, 10000);
179
- it('should use same UUID trace ID for addEventStart and addEventError on HTTP errors', async () => {
180
- const noRetryClient = httpBinClient.extend({
181
- retry: { limit: 0 }
182
- });
183
- await expect(noRetryClient.get('status/500')).rejects.toThrow();
184
- expect(mockedTracing.addEventStart).toHaveBeenCalledTimes(1);
185
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
186
- const startCall = mockedTracing.addEventStart.mock.calls[0][0];
187
- const errorCall = mockedTracing.addEventError.mock.calls[0][0];
188
- expect(isUuidFormat(startCall.id)).toBe(true);
189
- expect(isUuidFormat(errorCall.id)).toBe(true);
190
- expect(startCall.id).toBe(errorCall.id);
191
- }, 10000);
192
- it('should use same UUID trace ID for addEventStart and addEventError on timeout errors', async () => {
193
- const timeoutClient = httpClient({
194
- prefixUrl: 'https://httpbingo.org',
195
- timeout: 50
196
- });
197
- await expect(timeoutClient.get('delay/2')).rejects.toThrow();
198
- expect(mockedTracing.addEventStart).toHaveBeenCalledTimes(1);
199
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
200
- const startCall = mockedTracing.addEventStart.mock.calls[0][0];
201
- const errorCall = mockedTracing.addEventError.mock.calls[0][0];
202
- expect(isUuidFormat(startCall.id)).toBe(true);
203
- expect(isUuidFormat(errorCall.id)).toBe(true);
204
- expect(startCall.id).toBe(errorCall.id);
205
- }, 10000);
206
- it('should maintain trace ID consistency between ky hooks and wrapped fetch', async () => {
207
- const errorClient = httpClient({
208
- prefixUrl: 'https://httpbingo.org',
209
- timeout: 1,
210
- retry: { limit: 0 }
211
- });
212
- try {
213
- await errorClient.get('delay/5');
214
- expect.fail('Should have thrown timeout error');
215
- }
216
- catch {
217
- // Expected timeout error
218
- }
219
- expect(mockedTracing.addEventStart).toHaveBeenCalledTimes(1);
220
- expect(mockedTracing.addEventError).toHaveBeenCalledTimes(1);
221
- const startCall = mockedTracing.addEventStart.mock.calls[0][0];
222
- const errorCall = mockedTracing.addEventError.mock.calls[0][0];
223
- expect(isUuidFormat(startCall.id)).toBe(true);
224
- expect(isUuidFormat(errorCall.id)).toBe(true);
225
- expect(startCall.id).toBe(errorCall.id);
226
- }, 10000);
227
- it('should always use UUID format when assignRequestId hook is present', async () => {
228
- const scenarios = [
229
- { method: 'GET', url: 'anything/test-1' },
230
- { method: 'POST', url: 'anything/test-2', body: { data: 'test' } },
231
- { method: 'PUT', url: 'anything/test-3', body: { update: true } },
232
- { method: 'DELETE', url: 'anything/test-4' }
233
- ];
234
- for (const scenario of scenarios) {
235
- mockedTracing.addEventStart.mockClear();
236
- mockedTracing.addEventEnd.mockClear();
237
- if (scenario.method === 'GET') {
238
- await httpBinClient.get(scenario.url);
239
- }
240
- else if (scenario.method === 'POST') {
241
- await httpBinClient.post(scenario.url, { json: scenario.body });
242
- }
243
- else if (scenario.method === 'PUT') {
244
- await httpBinClient.put(scenario.url, { json: scenario.body });
245
- }
246
- else if (scenario.method === 'DELETE') {
247
- await httpBinClient.delete(scenario.url);
248
- }
249
- const startCall = mockedTracing.addEventStart.mock.calls[0][0];
250
- const endCall = mockedTracing.addEventEnd.mock.calls[0][0];
251
- expect(isUuidFormat(startCall.id)).toBe(true);
252
- expect(isUuidFormat(endCall.id)).toBe(true);
253
- expect(startCall.id).toBe(endCall.id);
254
- }
255
- }, 15000);
256
- });
257
- describe('No Tracing Without X-Request-ID', () => {
258
- beforeEach(() => {
259
- mockedTracing.addEventStart.mockClear();
260
- mockedTracing.addEventEnd.mockClear();
261
- mockedTracing.addEventError.mockClear();
262
- vi.spyOn(console, 'warn').mockImplementation(() => { });
263
- });
264
- afterEach(() => {
265
- vi.restoreAllMocks();
266
- });
267
- it('should skip tracing when assignRequestId hook is not present', async () => {
268
- const noUuidClient = ky.create({
269
- prefixUrl: 'https://httpbingo.org',
270
- hooks: {
271
- beforeRequest: [
272
- traceRequest
273
- ],
274
- afterResponse: [
275
- traceResponse
276
- ],
277
- beforeError: [
278
- traceError
279
- ]
280
- }
281
- });
282
- const wrappedNoUuidClient = noUuidClient.extend({
283
- fetch: applyFetchErrorTracing(globalThis.fetch)
284
- });
285
- const response = await wrappedNoUuidClient.get('anything/no-trace-test');
286
- expect(response.status).toBe(200);
287
- // No tracing should occur without X-Request-ID
288
- expect(mockedTracing.addEventStart).not.toHaveBeenCalled();
289
- expect(mockedTracing.addEventEnd).not.toHaveBeenCalled();
290
- // Warning should be logged
291
- expect(console.warn).toHaveBeenCalledWith('createTraceId: X-Request-ID header not found. Tracing will be skipped for this request.');
292
- }, 10000);
293
- it('should skip tracing for errors when X-Request-ID is missing', async () => {
294
- const noUuidClient = ky.create({
295
- prefixUrl: 'https://httpbingo.org',
296
- hooks: {
297
- beforeRequest: [traceRequest],
298
- afterResponse: [traceResponse],
299
- beforeError: [traceError]
300
- },
301
- retry: { limit: 0 }
302
- }).extend({
303
- fetch: applyFetchErrorTracing(globalThis.fetch)
304
- });
305
- await expect(noUuidClient.get('status/500')).rejects.toThrow();
306
- // No error tracing should occur without X-Request-ID
307
- expect(mockedTracing.addEventStart).not.toHaveBeenCalled();
308
- expect(mockedTracing.addEventError).not.toHaveBeenCalled();
309
- // Warning should be logged
310
- expect(console.warn).toHaveBeenCalled();
311
- }, 10000);
312
- });
313
- describe('Trace ID Generation - Request Object Validation', () => {
314
- beforeEach(() => {
315
- mockedTracing.addEventStart.mockClear();
316
- mockedTracing.addEventEnd.mockClear();
317
- mockedTracing.addEventError.mockClear();
318
- });
319
- it('should pass a valid Request object to createTraceId', async () => {
320
- const response = await httpBinClient.get('anything/trace-validation');
321
- expect(response.status).toBe(200);
322
- expect(mockedTracing.addEventStart).toHaveBeenCalled();
323
- const startCall = mockedTracing.addEventStart.mock.calls[0][0];
324
- const traceId = startCall.id;
325
- // Trace ID should be a UUID from X-Request-ID header
326
- expect(isUuidFormat(traceId)).toBe(true);
327
- // An empty object would return null (no X-Request-ID header)
328
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
329
- const emptyObjectResult = createTraceId({});
330
- expect(emptyObjectResult).toBeNull();
331
- warnSpy.mockRestore();
332
- }, 10000);
333
- it('should produce different trace IDs for GET vs POST to same endpoint', async () => {
334
- mockedTracing.addEventStart.mockClear();
335
- // Make GET request to specific endpoint
336
- const getResponse = await httpBinClient.get('anything/trace-same-endpoint');
337
- expect(getResponse.status).toBe(200);
338
- expect(mockedTracing.addEventStart).toHaveBeenCalled();
339
- const getTraceId = mockedTracing.addEventStart.mock.calls[0][0].id;
340
- mockedTracing.addEventStart.mockClear();
341
- // Make POST request to SAME endpoint
342
- const postResponse = await httpBinClient.post('anything/trace-same-endpoint', { json: { data: 'test' } });
343
- expect(postResponse.status).toBe(200);
344
- expect(mockedTracing.addEventStart).toHaveBeenCalled();
345
- const postTraceId = mockedTracing.addEventStart.mock.calls[0][0].id;
346
- expect(getTraceId).not.toBe(postTraceId);
347
- expect(isUuidFormat(getTraceId)).toBe(true);
348
- expect(isUuidFormat(postTraceId)).toBe(true);
349
- }, 10000);
350
- it('should generate unique trace IDs for identical requests', async () => {
351
- mockedTracing.addEventStart.mockClear();
352
- const response1 = await httpBinClient.get('anything/unique-requests-1');
353
- expect(response1.status).toBe(200);
354
- expect(mockedTracing.addEventStart).toHaveBeenCalled();
355
- const traceId1 = mockedTracing.addEventStart.mock.calls[0][0].id;
356
- mockedTracing.addEventStart.mockClear();
357
- const response2 = await httpBinClient.get('anything/unique-requests-1');
358
- expect(response2.status).toBe(200);
359
- expect(mockedTracing.addEventStart).toHaveBeenCalled();
360
- const traceId2 = mockedTracing.addEventStart.mock.calls[0][0].id;
361
- expect(isUuidFormat(traceId1)).toBe(true);
362
- expect(isUuidFormat(traceId2)).toBe(true);
363
- expect(traceId1).not.toBe(traceId2);
364
- }, 10000);
365
- it('should maintain trace ID consistency during timeout errors', async () => {
366
- const timeoutClient = httpClient({
367
- prefixUrl: 'https://httpbingo.org',
368
- timeout: 300
369
- });
370
- mockedTracing.addEventStart.mockClear();
371
- mockedTracing.addEventError.mockClear();
372
- try {
373
- await timeoutClient.get('delay/3');
374
- expect.fail('Should have thrown timeout error');
375
- }
376
- catch {
377
- expect(mockedTracing.addEventStart).toHaveBeenCalled();
378
- const startCall = mockedTracing.addEventStart.mock.calls[0][0];
379
- const requestTraceId = startCall.id;
380
- expect(mockedTracing.addEventError).toHaveBeenCalled();
381
- const errorCall = mockedTracing.addEventError.mock.calls[0][0];
382
- const errorTraceId = errorCall.id;
383
- expect(isUuidFormat(requestTraceId)).toBe(true);
384
- expect(isUuidFormat(errorTraceId)).toBe(true);
385
- expect(requestTraceId).toBe(errorTraceId);
386
- }
387
- }, 10000);
388
- });
389
- });
package/dist/index.js DELETED
@@ -1,51 +0,0 @@
1
- import ky from 'ky';
2
- import { assignRequestId, traceRequest, traceResponse, traceError } from './hooks/index.js';
3
- import { applyFetchErrorTracing } from '#hooks/trace_error.js';
4
- const baseHttpClient = ky.create({
5
- hooks: {
6
- beforeRequest: [
7
- assignRequestId,
8
- traceRequest
9
- ],
10
- afterResponse: [
11
- traceResponse
12
- ],
13
- beforeError: [
14
- traceError
15
- ]
16
- }
17
- });
18
- const applyDefaultOptions = (userOptions) => (parentOptions) => {
19
- const kyFetch = parentOptions.fetch || globalThis.fetch.bind(globalThis);
20
- const patchedFetch = applyFetchErrorTracing(kyFetch);
21
- return {
22
- fetch: patchedFetch,
23
- ...userOptions
24
- };
25
- };
26
- /**
27
- * Creates a ky client.
28
- *
29
- * This client is customized with hooks to integrate with Output.ai tracing.
30
- *
31
- * @example
32
- * ```ts
33
- * import { httpClient } from '@outputai/http';
34
- *
35
- * const client = httpClient({
36
- * prefixUrl: 'https://api.example.com',
37
- * timeout: 30000,
38
- * retry: { limit: 3 }
39
- * });
40
- *
41
- * const response = await client.get('users/1');
42
- * const data = await response.json();
43
- * ```
44
- *
45
- * @param options - The ky options to extend the base client.
46
- * @returns A ky instance extended with Output.ai tracing hooks.
47
- */
48
- export function httpClient(options = {}) {
49
- return baseHttpClient.extend(applyDefaultOptions(options));
50
- }
51
- export { HTTPError, TimeoutError } from 'ky';
@@ -1 +0,0 @@
1
- export {};