@output.ai/http 0.0.1 → 0.0.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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/hooks/index.d.ts +3 -3
  3. package/dist/hooks/index.js +3 -3
  4. package/dist/hooks/{trace-error.d.ts → trace_error.d.ts} +1 -1
  5. package/dist/hooks/trace_error.js +16 -0
  6. package/dist/hooks/trace_error.spec.js +35 -0
  7. package/dist/hooks/{trace-request.d.ts → trace_request.d.ts} +1 -1
  8. package/dist/hooks/trace_request.js +19 -0
  9. package/dist/hooks/trace_request.spec.d.ts +1 -0
  10. package/dist/hooks/trace_request.spec.js +58 -0
  11. package/dist/hooks/{trace-response.d.ts → trace_response.d.ts} +1 -1
  12. package/dist/hooks/trace_response.js +21 -0
  13. package/dist/hooks/trace_response.spec.d.ts +1 -0
  14. package/dist/hooks/trace_response.spec.js +65 -0
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +3 -3
  17. package/dist/index.spec.js +15 -24
  18. package/dist/utils/create_trace_id.d.ts +8 -0
  19. package/dist/utils/create_trace_id.js +10 -0
  20. package/dist/utils/create_trace_id.spec.d.ts +1 -0
  21. package/dist/utils/create_trace_id.spec.js +12 -0
  22. package/dist/utils/index.d.ts +4 -1
  23. package/dist/utils/index.js +4 -1
  24. package/dist/utils/parse_request_body.d.ts +8 -0
  25. package/dist/utils/parse_request_body.js +19 -0
  26. package/dist/utils/parse_request_body.spec.d.ts +1 -0
  27. package/dist/utils/parse_request_body.spec.js +19 -0
  28. package/dist/utils/parse_response_body.d.ts +10 -0
  29. package/dist/utils/parse_response_body.js +14 -0
  30. package/dist/utils/parse_response_body.spec.d.ts +1 -0
  31. package/dist/utils/parse_response_body.spec.js +19 -0
  32. package/dist/utils/{redact-headers.d.ts → redact_headers.d.ts} +1 -1
  33. package/dist/utils/{redact-headers.js → redact_headers.js} +2 -2
  34. package/dist/utils/redact_headers.spec.d.ts +1 -0
  35. package/dist/utils/{redact-headers.spec.js → redact_headers.spec.js} +1 -1
  36. package/package.json +6 -6
  37. package/dist/hooks/trace-error.js +0 -34
  38. package/dist/hooks/trace-request.js +0 -42
  39. package/dist/hooks/trace-response.js +0 -45
  40. /package/dist/{utils/redact-headers.spec.d.ts → hooks/trace_error.spec.d.ts} +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @output.ai/http
2
2
 
3
- TypeScript HTTP client that wraps [ky](https://github.com/sindresorhus/ky) with Flow SDK tracing hooks.
3
+ TypeScript HTTP client that wraps [ky](https://github.com/sindresorhus/ky) with Output SDK tracing hooks.
4
4
 
5
5
  ## Installation
6
6
 
@@ -1,3 +1,3 @@
1
- export { traceRequest } from './trace-request.js';
2
- export { traceResponse } from './trace-response.js';
3
- export { traceError } from './trace-error.js';
1
+ export { traceRequest } from './trace_request.js';
2
+ export { traceResponse } from './trace_response.js';
3
+ export { traceError } from './trace_error.js';
@@ -1,3 +1,3 @@
1
- export { traceRequest } from './trace-request.js';
2
- export { traceResponse } from './trace-response.js';
3
- export { traceError } from './trace-error.js';
1
+ export { traceRequest } from './trace_request.js';
2
+ export { traceResponse } from './trace_response.js';
3
+ export { traceError } from './trace_error.js';
@@ -1,5 +1,5 @@
1
1
  import type { BeforeErrorHook } from 'ky';
2
2
  /**
3
- * Traces HTTP errors for observability using Flow SDK tracing
3
+ * Traces HTTP errors for observability using Output SDK tracing
4
4
  */
5
5
  export declare const traceError: BeforeErrorHook;
@@ -0,0 +1,16 @@
1
+ import { createTraceId, redactHeaders } from '#utils/index.js';
2
+ import { Tracing } from '@output.ai/core/tracing';
3
+ /**
4
+ * Traces HTTP errors for observability using Output SDK tracing
5
+ */
6
+ export const traceError = (error) => {
7
+ Tracing.addEventError({
8
+ id: createTraceId(error.request),
9
+ details: {
10
+ status: error.response.status,
11
+ statusText: error.response.statusText,
12
+ headers: redactHeaders(Object.fromEntries(error.response.headers.entries()))
13
+ }
14
+ });
15
+ return error;
16
+ };
@@ -0,0 +1,35 @@
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 '@output.ai/core/tracing';
5
+ vi.mock('../utils/index.js', () => ({
6
+ redactHeaders: vi.fn((h) => h),
7
+ createTraceId: vi.fn(() => 'trace-id')
8
+ }));
9
+ vi.mock('@output.ai/core/tracing', () => ({
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);
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 +1,6 @@
1
1
  import type { BeforeRequestHook } from 'ky';
2
2
  /**
3
- * Traces HTTP request for observability using Flow SDK tracing
3
+ * Traces HTTP request for observability using Output SDK tracing
4
4
  * Respects LOG_HTTP_VERBOSE environment variable for detailed logging
5
5
  */
6
6
  export declare const traceRequest: BeforeRequestHook;
@@ -0,0 +1,19 @@
1
+ import { Tracing } from '@output.ai/core/tracing';
2
+ import { redactHeaders, createTraceId, parseRequestBody } from '#utils/index.js';
3
+ import { config } from '#config.js';
4
+ /**
5
+ * Traces HTTP request for observability using Output SDK tracing
6
+ * Respects LOG_HTTP_VERBOSE environment variable for detailed logging
7
+ */
8
+ export const traceRequest = async (request, _options) => {
9
+ const details = {
10
+ method: request.method,
11
+ url: request.url
12
+ };
13
+ if (config.logVerbose) {
14
+ const headers = Object.fromEntries(request.headers.entries());
15
+ details.headers = redactHeaders(headers);
16
+ details.body = await parseRequestBody(request);
17
+ }
18
+ Tracing.addEventStart({ id: createTraceId(request), kind: 'http', name: 'request', details });
19
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { traceRequest } from './trace_request.js';
3
+ import { Tracing } from '@output.ai/core/tracing';
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('@output.ai/core/tracing', () => ({
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
+ await traceRequest(request, options);
30
+ expect(mockedTracing.addEventStart).toHaveBeenCalledTimes(1);
31
+ const arg = mockedTracing.addEventStart.mock.calls[0][0];
32
+ expect(arg).toHaveProperty('kind', 'http');
33
+ expect(arg).toHaveProperty('name', 'request');
34
+ expect(arg.details).toEqual({
35
+ method: 'GET',
36
+ url: 'https://api.example.com/users/1'
37
+ });
38
+ });
39
+ it('traces headers and parsed body when verbose logging is enabled', async () => {
40
+ mockedConfig.logVerbose = true;
41
+ const request = new Request('https://api.example.com/users', {
42
+ method: 'POST',
43
+ headers: {
44
+ authorization: 'secret',
45
+ 'x-custom': 'value'
46
+ },
47
+ body: JSON.stringify({ name: 'test' })
48
+ });
49
+ const options = {};
50
+ await traceRequest(request, options);
51
+ expect(mockedTracing.addEventStart).toHaveBeenCalledTimes(1);
52
+ const arg = mockedTracing.addEventStart.mock.calls[0][0];
53
+ expect(arg.details.method).toBe('POST');
54
+ expect(arg.details.url).toBe('https://api.example.com/users');
55
+ expect(arg.details.headers).toMatchObject({ authorization: 'secret', 'x-custom': 'value' });
56
+ expect(arg.details.body).toEqual({ mocked: true });
57
+ });
58
+ });
@@ -1,6 +1,6 @@
1
1
  import type { AfterResponseHook } from 'ky';
2
2
  /**
3
- * Traces HTTP response for observability using Flow SDK tracing
3
+ * Traces HTTP response for observability using Output SDK tracing
4
4
  * Respects LOG_HTTP_VERBOSE environment variable for detailed logging
5
5
  */
6
6
  export declare const traceResponse: AfterResponseHook;
@@ -0,0 +1,21 @@
1
+ import { Tracing } from '@output.ai/core/tracing';
2
+ import { redactHeaders, createTraceId, parseResponseBody } from '#utils/index.js';
3
+ ;
4
+ import { config } from '#config.js';
5
+ /**
6
+ * Traces HTTP response for observability using Output SDK tracing
7
+ * Respects LOG_HTTP_VERBOSE environment variable for detailed logging
8
+ */
9
+ export const traceResponse = async (request, _options, response) => {
10
+ const details = {
11
+ status: response.status,
12
+ statusText: response.statusText
13
+ };
14
+ if (config.logVerbose) {
15
+ const responseHeaders = Object.fromEntries(response.headers.entries());
16
+ details.headers = redactHeaders(responseHeaders);
17
+ details.body = await parseResponseBody(response);
18
+ }
19
+ Tracing.addEventEnd({ id: createTraceId(request), details });
20
+ return response;
21
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { traceResponse } from './trace_response.js';
3
+ import { Tracing } from '@output.ai/core/tracing';
4
+ import { config } from '../config.js';
5
+ vi.mock('../utils/index.js', () => ({
6
+ redactHeaders: vi.fn((h) => h),
7
+ parseResponseBody: vi.fn(async () => ({ mocked: true })),
8
+ createTraceId: vi.fn(() => 'trace-id')
9
+ }));
10
+ vi.mock('@output.ai/core/tracing', () => ({
11
+ Tracing: {
12
+ addEventEnd: 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_response', () => {
21
+ beforeEach(() => {
22
+ mockedTracing.addEventEnd.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 response = new Response('ok', { status: 200, statusText: 'OK' });
29
+ const options = {};
30
+ const result = await traceResponse(request, options, response);
31
+ expect(result).toBe(response);
32
+ expect(mockedTracing.addEventEnd).toHaveBeenCalledTimes(1);
33
+ const arg = mockedTracing.addEventEnd.mock.calls[0][0];
34
+ expect(arg.details).toEqual({ status: 200, statusText: 'OK' });
35
+ });
36
+ it('traces headers and parsed JSON body when verbose logging is enabled', async () => {
37
+ mockedConfig.logVerbose = true;
38
+ const request = new Request('https://api.example.com/users', { method: 'POST' });
39
+ const response = new Response(JSON.stringify({ success: true }), {
40
+ status: 201,
41
+ statusText: 'Created',
42
+ headers: { 'content-type': 'application/json', authorization: 'secret', 'x-custom': 'v' }
43
+ });
44
+ const options = {};
45
+ await traceResponse(request, options, response);
46
+ const arg = mockedTracing.addEventEnd.mock.calls[0][0];
47
+ expect(arg.details.status).toBe(201);
48
+ expect(arg.details.statusText).toBe('Created');
49
+ expect(arg.details.headers).toEqual({ 'content-type': 'application/json', authorization: 'secret', 'x-custom': 'v' });
50
+ expect(arg.details.body).toEqual({ mocked: true });
51
+ });
52
+ it('traces text body for non-JSON content types', async () => {
53
+ mockedConfig.logVerbose = true;
54
+ const request = new Request('https://api.example.com/ping', { method: 'GET' });
55
+ const response = new Response('pong', {
56
+ status: 200,
57
+ statusText: 'OK',
58
+ headers: { 'content-type': 'text/plain' }
59
+ });
60
+ const options = {};
61
+ await traceResponse(request, options, response);
62
+ const arg = mockedTracing.addEventEnd.mock.calls[0][0];
63
+ expect(arg.details.body).toEqual({ mocked: true });
64
+ });
65
+ });
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { Options } from 'ky';
2
2
  /**
3
- * Creates an HTTP client with Flow SDK tracing
3
+ * Creates an HTTP client with Output SDK tracing
4
4
  *
5
5
  * @param options - ky options to extend the base client with
6
- * @returns Extended ky instance with Flow SDK hooks
6
+ * @returns Extended ky instance with Output SDK hooks
7
7
  *
8
8
  * @example
9
9
  * ```typescript
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import ky from 'ky';
2
2
  import { traceRequest, traceResponse, traceError } from './hooks/index.js';
3
- // Create base HTTP client with standard Flow SDK tracing hooks
3
+ // Create base HTTP client with standard Output SDK tracing hooks
4
4
  const baseHttpClient = ky.create({
5
5
  hooks: {
6
6
  beforeRequest: [
@@ -15,10 +15,10 @@ const baseHttpClient = ky.create({
15
15
  }
16
16
  });
17
17
  /**
18
- * Creates an HTTP client with Flow SDK tracing
18
+ * Creates an HTTP client with Output SDK tracing
19
19
  *
20
20
  * @param options - ky options to extend the base client with
21
- * @returns Extended ky instance with Flow SDK hooks
21
+ * @returns Extended ky instance with Output SDK hooks
22
22
  *
23
23
  * @example
24
24
  * ```typescript
@@ -1,9 +1,13 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { httpClient, HTTPError, TimeoutError } from './index.js';
3
- import { trace } from '@output.ai/trace';
3
+ import { Tracing } from '@output.ai/core/tracing';
4
4
  import { config } from './config.js';
5
- vi.mock('@output.ai/trace', () => ({
6
- trace: vi.fn()
5
+ vi.mock('@output.ai/core/tracing', () => ({
6
+ Tracing: {
7
+ addEventStart: vi.fn(),
8
+ addEventEnd: vi.fn(),
9
+ addEventError: vi.fn()
10
+ }
7
11
  }));
8
12
  vi.mock('./config.js', () => ({
9
13
  config: {
@@ -90,11 +94,13 @@ vi.mock('ky', () => {
90
94
  }
91
95
  };
92
96
  });
93
- const mockedTrace = trace;
97
+ const mockedTracing = vi.mocked(Tracing, true);
94
98
  const mockedConfig = vi.mocked(config);
95
99
  describe('HTTP Client', () => {
96
100
  beforeEach(() => {
97
- mockedTrace.mockClear();
101
+ mockedTracing.addEventStart.mockClear();
102
+ mockedTracing.addEventEnd.mockClear();
103
+ mockedTracing.addEventError.mockClear();
98
104
  });
99
105
  describe('httpClient function', () => {
100
106
  it('should create an HTTP client with default options', () => {
@@ -148,13 +154,7 @@ describe('HTTP Client', () => {
148
154
  prefixUrl: 'https://api.example.com'
149
155
  });
150
156
  await client.get('users/1');
151
- expect(mockedTrace).toHaveBeenCalled();
152
- const traceCall = mockedTrace.mock.calls.find(call => call[0].event === 'http.request');
153
- expect(traceCall).toBeDefined();
154
- if (traceCall) {
155
- expect(traceCall[0].input.headers).toBeUndefined();
156
- expect(traceCall[0].input.body).toBeUndefined();
157
- }
157
+ expect(mockedTracing.addEventStart).toHaveBeenCalled();
158
158
  });
159
159
  it('should trace headers and bodies when verbose logging is enabled', async () => {
160
160
  mockedConfig.logVerbose = true;
@@ -162,13 +162,7 @@ describe('HTTP Client', () => {
162
162
  prefixUrl: 'https://api.example.com'
163
163
  });
164
164
  await client.post('users', { json: { name: 'test', email: 'test@example.com' } });
165
- expect(mockedTrace).toHaveBeenCalled();
166
- const requestTraceCall = mockedTrace.mock.calls.find(call => call[0].event === 'http.request');
167
- expect(requestTraceCall).toBeDefined();
168
- if (requestTraceCall) {
169
- expect(requestTraceCall[0].input.headers).toBeDefined();
170
- expect(requestTraceCall[0].input.body).toBeDefined();
171
- }
165
+ expect(mockedTracing.addEventStart).toHaveBeenCalled();
172
166
  });
173
167
  });
174
168
  describe('Hook Preservation', () => {
@@ -204,11 +198,8 @@ describe('HTTP Client', () => {
204
198
  await extendedClient.get('users/1');
205
199
  expect(customBeforeRequestCalled).toHaveBeenCalled();
206
200
  expect(customAfterResponseCalled).toHaveBeenCalled();
207
- expect(mockedTrace).toHaveBeenCalled();
208
- const requestTraceCall = mockedTrace.mock.calls.find(call => call[0].event === 'http.request');
209
- const responseTraceCall = mockedTrace.mock.calls.find(call => call[0].event === 'http.response');
210
- expect(requestTraceCall).toBeDefined();
211
- expect(responseTraceCall).toBeDefined();
201
+ expect(mockedTracing.addEventStart).toHaveBeenCalled();
202
+ expect(mockedTracing.addEventEnd).toHaveBeenCalled();
212
203
  });
213
204
  });
214
205
  describe('Mocking Verification', () => {
@@ -0,0 +1,8 @@
1
+ import type { KyRequest } from 'ky';
2
+ /**
3
+ * Created an trace id based on the KyRequest
4
+ *
5
+ * @param {KyRequest} request
6
+ * @returns {string} A unique trace id
7
+ */
8
+ export default function createTraceId(request: KyRequest): string;
@@ -0,0 +1,10 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Created an trace id based on the KyRequest
4
+ *
5
+ * @param {KyRequest} request
6
+ * @returns {string} A unique trace id
7
+ */
8
+ export default function createTraceId(request) {
9
+ return createHash('md5').update(JSON.stringify(request)).digest('hex');
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import createTraceId from './create_trace_id.js';
3
+ describe('utils/create_trace_id', () => {
4
+ it('creates a deterministic md5 hash id from request', () => {
5
+ const req = new Request('https://ex.com/users/1', { method: 'GET' });
6
+ const id1 = createTraceId(req);
7
+ const id2 = createTraceId(req);
8
+ expect(typeof id1).toBe('string');
9
+ expect(id1).toHaveLength(32);
10
+ expect(id1).toBe(id2);
11
+ });
12
+ });
@@ -1 +1,4 @@
1
- export { redactHeaders } from './redact-headers.js';
1
+ export { default as parseResponseBody } from './parse_response_body.js';
2
+ export { default as parseRequestBody } from './parse_request_body.js';
3
+ export { default as redactHeaders } from './redact_headers.js';
4
+ export { default as createTraceId } from './create_trace_id.js';
@@ -1 +1,4 @@
1
- export { redactHeaders } from './redact-headers.js';
1
+ export { default as parseResponseBody } from './parse_response_body.js';
2
+ export { default as parseRequestBody } from './parse_request_body.js';
3
+ export { default as redactHeaders } from './redact_headers.js';
4
+ export { default as createTraceId } from './create_trace_id.js';
@@ -0,0 +1,8 @@
1
+ import type { KyRequest } from 'ky';
2
+ /**
3
+ * Safely parses request body as JSON, falling back to string if parsing fails
4
+ *
5
+ * @param {KyRequest} request
6
+ * @returns {object|string|null} The parsed response
7
+ */
8
+ export default function parseRequestBody(request: KyRequest): Promise<object | string | null>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Safely parses request body as JSON, falling back to string if parsing fails
3
+ *
4
+ * @param {KyRequest} request
5
+ * @returns {object|string|null} The parsed response
6
+ */
7
+ export default async function parseRequestBody(request) {
8
+ if (!request.body) {
9
+ return null;
10
+ }
11
+ const cloned = request.clone();
12
+ const body = await cloned.text();
13
+ try {
14
+ return JSON.parse(body);
15
+ }
16
+ catch {
17
+ return body;
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import parseRequestBody from './parse_request_body.js';
3
+ describe('utils/parse_request_body', () => {
4
+ it('returns null when no body is present', async () => {
5
+ const req = new Request('https://ex.com', { method: 'GET' });
6
+ const result = await parseRequestBody(req);
7
+ expect(result).toBeNull();
8
+ });
9
+ it('parses JSON body when present', async () => {
10
+ const req = new Request('https://ex.com', { method: 'POST', body: JSON.stringify({ a: 1 }) });
11
+ const result = await parseRequestBody(req);
12
+ expect(result).toEqual({ a: 1 });
13
+ });
14
+ it('returns raw text when not valid JSON', async () => {
15
+ const req = new Request('https://ex.com', { method: 'POST', body: 'not-json' });
16
+ const result = await parseRequestBody(req);
17
+ expect(result).toBe('not-json');
18
+ });
19
+ });
@@ -0,0 +1,10 @@
1
+ import type { KyResponse } from 'ky';
2
+ /**
3
+ * Parses response body based on content type:
4
+ * - application/json = object
5
+ * - text/plain = string
6
+ *
7
+ * @param {KyResponse} response
8
+ * @returns {object|string|null} The parsed response
9
+ */
10
+ export default function parseResponseBody(response: KyResponse): Promise<object | string | null>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Parses response body based on content type:
3
+ * - application/json = object
4
+ * - text/plain = string
5
+ *
6
+ * @param {KyResponse} response
7
+ * @returns {object|string|null} The parsed response
8
+ */
9
+ export default async function parseResponseBody(response) {
10
+ const cloned = response.clone();
11
+ const contentType = response.headers.get('content-type') || '';
12
+ const body = await cloned[contentType.includes('application/json') ? 'json' : 'text']();
13
+ return body || null;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import parseResponseBody from './parse_response_body.js';
3
+ describe('utils/parse_response_body', () => {
4
+ it('parses JSON when content-type is application/json', async () => {
5
+ const res = new Response(JSON.stringify({ ok: true }), { headers: { 'content-type': 'application/json' } });
6
+ const result = await parseResponseBody(res);
7
+ expect(result).toEqual({ ok: true });
8
+ });
9
+ it('returns text when content-type is not JSON', async () => {
10
+ const res = new Response('hello', { headers: { 'content-type': 'text/plain' } });
11
+ const result = await parseResponseBody(res);
12
+ expect(result).toBe('hello');
13
+ });
14
+ it('returns null for empty body', async () => {
15
+ const res = new Response('', { headers: { 'content-type': 'text/plain' } });
16
+ const result = await parseResponseBody(res);
17
+ expect(result).toBeNull();
18
+ });
19
+ });
@@ -3,4 +3,4 @@
3
3
  * @param headers - Headers object to redact
4
4
  * @returns Object with sensitive headers redacted
5
5
  */
6
- export declare function redactHeaders(headers: Record<string, string> | Headers): Record<string, string>;
6
+ export default function redactHeaders(headers: Record<string, string> | Headers): Record<string, string>;
@@ -5,9 +5,9 @@ const SENSITIVE_HEADER_PATTERNS = [
5
5
  /authorization/i,
6
6
  /token/i,
7
7
  /api-?key/i,
8
- /apikey/i,
9
8
  /secret/i,
10
9
  /password/i,
10
+ /pwd/i,
11
11
  /key/i,
12
12
  /cookie/i
13
13
  ];
@@ -16,7 +16,7 @@ const SENSITIVE_HEADER_PATTERNS = [
16
16
  * @param headers - Headers object to redact
17
17
  * @returns Object with sensitive headers redacted
18
18
  */
19
- export function redactHeaders(headers) {
19
+ export default function redactHeaders(headers) {
20
20
  const result = {};
21
21
  const entries = headers instanceof Headers ? headers.entries() : Object.entries(headers);
22
22
  for (const [key, value] of entries) {
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { redactHeaders } from './redact-headers.js';
2
+ import redactHeaders from './redact_headers.js';
3
3
  describe('redactHeaders', () => {
4
4
  describe('with Record<string, string> input', () => {
5
5
  it('should redact sensitive headers (case insensitive)', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/http",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Framework abstraction to make HTTP calls with tracing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,12 +11,12 @@
11
11
  "scripts": {
12
12
  "build": "rm -rf ./dist && tsc"
13
13
  },
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/growthxai/flow-sdk"
17
- },
18
14
  "dependencies": {
19
- "@output.ai/trace": "0.0.1",
15
+ "@output.ai/core": ">=0.0.1",
20
16
  "ky": "~1.9.1"
17
+ },
18
+ "license": "UNLICENSED",
19
+ "imports": {
20
+ "#*": "./dist/*"
21
21
  }
22
22
  }
@@ -1,34 +0,0 @@
1
- import { HTTPError } from 'ky';
2
- import { trace } from '@output.ai/trace';
3
- import { redactHeaders } from '../utils/index.js';
4
- /**
5
- * Traces HTTP errors for observability using Flow SDK tracing
6
- */
7
- export const traceError = async (error) => {
8
- const request = error.request;
9
- const response = error.response;
10
- const requestHeaders = Object.fromEntries(request.headers.entries());
11
- trace({
12
- lib: 'flow-http',
13
- event: 'http.error',
14
- input: {
15
- method: request.method,
16
- url: request.url,
17
- requestHeaders: redactHeaders(requestHeaders)
18
- },
19
- output: {
20
- error: error instanceof HTTPError && response ? {
21
- name: error.name,
22
- message: error.message,
23
- status: response.status,
24
- statusText: response.statusText,
25
- headers: redactHeaders(Object.fromEntries(response.headers.entries()))
26
- } : {
27
- name: error.name,
28
- message: error.message,
29
- type: 'network'
30
- }
31
- }
32
- });
33
- return error;
34
- };
@@ -1,42 +0,0 @@
1
- import { trace } from '@output.ai/trace';
2
- import { redactHeaders } from '../utils/index.js';
3
- import { config } from '../config.js';
4
- /**
5
- * Safely parses request body as JSON, falling back to string if parsing fails
6
- */
7
- async function safeParseBody(request) {
8
- if (!request.body) {
9
- return null;
10
- }
11
- const cloned = request.clone();
12
- const body = await cloned.text();
13
- if (!body) {
14
- return null;
15
- }
16
- try {
17
- return JSON.parse(body);
18
- }
19
- catch {
20
- return body;
21
- }
22
- }
23
- /**
24
- * Traces HTTP request for observability using Flow SDK tracing
25
- * Respects LOG_HTTP_VERBOSE environment variable for detailed logging
26
- */
27
- export const traceRequest = async (request) => {
28
- const traceData = {
29
- method: request.method,
30
- url: request.url
31
- };
32
- if (config.logVerbose) {
33
- const headers = Object.fromEntries(request.headers.entries());
34
- traceData.headers = redactHeaders(headers);
35
- traceData.body = await safeParseBody(request);
36
- }
37
- trace({
38
- lib: 'flow-http',
39
- event: 'http.request',
40
- input: traceData
41
- });
42
- };
@@ -1,45 +0,0 @@
1
- import { trace } from '@output.ai/trace';
2
- import { redactHeaders } from '../utils/index.js';
3
- import { config } from '../config.js';
4
- /**
5
- * Safely parses response body based on content type
6
- */
7
- async function safeParseResponseBody(response) {
8
- const cloned = response.clone();
9
- const contentType = response.headers.get('content-type') || '';
10
- const body = contentType.includes('application/json') ?
11
- await cloned.json() :
12
- await cloned.text();
13
- if (body !== null && body !== undefined && body !== '') {
14
- return body;
15
- }
16
- return null;
17
- }
18
- /**
19
- * Traces HTTP response for observability using Flow SDK tracing
20
- * Respects LOG_HTTP_VERBOSE environment variable for detailed logging
21
- */
22
- export const traceResponse = async (request, options, response) => {
23
- const inputData = {
24
- method: request.method,
25
- url: request.url
26
- };
27
- const outputData = {
28
- status: response.status,
29
- statusText: response.statusText
30
- };
31
- if (config.logVerbose) {
32
- const requestHeaders = Object.fromEntries(request.headers.entries());
33
- const responseHeaders = Object.fromEntries(response.headers.entries());
34
- inputData.requestHeaders = redactHeaders(requestHeaders);
35
- outputData.headers = redactHeaders(responseHeaders);
36
- outputData.body = await safeParseResponseBody(response);
37
- }
38
- trace({
39
- lib: 'flow-http',
40
- event: 'http.response',
41
- input: inputData,
42
- output: outputData
43
- });
44
- return response;
45
- };