@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.
- package/README.md +1 -1
- package/dist/hooks/index.d.ts +3 -3
- package/dist/hooks/index.js +3 -3
- package/dist/hooks/{trace-error.d.ts → trace_error.d.ts} +1 -1
- package/dist/hooks/trace_error.js +16 -0
- package/dist/hooks/trace_error.spec.js +35 -0
- package/dist/hooks/{trace-request.d.ts → trace_request.d.ts} +1 -1
- package/dist/hooks/trace_request.js +19 -0
- package/dist/hooks/trace_request.spec.d.ts +1 -0
- package/dist/hooks/trace_request.spec.js +58 -0
- package/dist/hooks/{trace-response.d.ts → trace_response.d.ts} +1 -1
- package/dist/hooks/trace_response.js +21 -0
- package/dist/hooks/trace_response.spec.d.ts +1 -0
- package/dist/hooks/trace_response.spec.js +65 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/index.spec.js +15 -24
- package/dist/utils/create_trace_id.d.ts +8 -0
- package/dist/utils/create_trace_id.js +10 -0
- package/dist/utils/create_trace_id.spec.d.ts +1 -0
- package/dist/utils/create_trace_id.spec.js +12 -0
- package/dist/utils/index.d.ts +4 -1
- package/dist/utils/index.js +4 -1
- package/dist/utils/parse_request_body.d.ts +8 -0
- package/dist/utils/parse_request_body.js +19 -0
- package/dist/utils/parse_request_body.spec.d.ts +1 -0
- package/dist/utils/parse_request_body.spec.js +19 -0
- package/dist/utils/parse_response_body.d.ts +10 -0
- package/dist/utils/parse_response_body.js +14 -0
- package/dist/utils/parse_response_body.spec.d.ts +1 -0
- package/dist/utils/parse_response_body.spec.js +19 -0
- package/dist/utils/{redact-headers.d.ts → redact_headers.d.ts} +1 -1
- package/dist/utils/{redact-headers.js → redact_headers.js} +2 -2
- package/dist/utils/redact_headers.spec.d.ts +1 -0
- package/dist/utils/{redact-headers.spec.js → redact_headers.spec.js} +1 -1
- package/package.json +6 -6
- package/dist/hooks/trace-error.js +0 -34
- package/dist/hooks/trace-request.js +0 -42
- package/dist/hooks/trace-response.js +0 -45
- /package/dist/{utils/redact-headers.spec.d.ts → hooks/trace_error.spec.d.ts} +0 -0
package/README.md
CHANGED
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { traceRequest } from './
|
|
2
|
-
export { traceResponse } from './
|
|
3
|
-
export { traceError } from './
|
|
1
|
+
export { traceRequest } from './trace_request.js';
|
|
2
|
+
export { traceResponse } from './trace_response.js';
|
|
3
|
+
export { traceError } from './trace_error.js';
|
package/dist/hooks/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { traceRequest } from './
|
|
2
|
-
export { traceResponse } from './
|
|
3
|
-
export { traceError } from './
|
|
1
|
+
export { traceRequest } from './trace_request.js';
|
|
2
|
+
export { traceResponse } from './trace_response.js';
|
|
3
|
+
export { traceError } from './trace_error.js';
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
21
|
+
* @returns Extended ky instance with Output SDK hooks
|
|
22
22
|
*
|
|
23
23
|
* @example
|
|
24
24
|
* ```typescript
|
package/dist/index.spec.js
CHANGED
|
@@ -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 {
|
|
3
|
+
import { Tracing } from '@output.ai/core/tracing';
|
|
4
4
|
import { config } from './config.js';
|
|
5
|
-
vi.mock('@output.ai/
|
|
6
|
-
|
|
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
|
|
97
|
+
const mockedTracing = vi.mocked(Tracing, true);
|
|
94
98
|
const mockedConfig = vi.mocked(config);
|
|
95
99
|
describe('HTTP Client', () => {
|
|
96
100
|
beforeEach(() => {
|
|
97
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
208
|
-
|
|
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,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
|
+
});
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
-
export {
|
|
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';
|
package/dist/utils/index.js
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
-
export {
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
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
|
-
};
|
|
File without changes
|