@outputai/http 0.10.1-next.f6a7c1a.0 → 0.11.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/events.d.ts +20 -0
- package/dist/fetch/events.js +4 -0
- package/dist/fetch/events.spec.js +64 -0
- package/dist/fetch/index.d.ts +10 -11
- package/dist/fetch/index.default_dispatcher.spec.d.ts +1 -0
- package/dist/fetch/index.default_dispatcher.spec.js +47 -0
- package/dist/fetch/index.js +32 -19
- package/dist/fetch/index.spec.js +114 -11
- package/dist/fetch/logger.d.ts +16 -66
- package/dist/fetch/logger.js +17 -70
- package/dist/fetch/logger.spec.js +77 -185
- package/dist/fetch/utils.d.ts +3 -45
- package/dist/fetch/utils.js +11 -35
- package/dist/fetch/utils.spec.js +46 -244
- package/dist/index.d.ts +5 -27
- package/dist/index.js +5 -29
- package/dist/ky/index.d.ts +24 -0
- package/dist/ky/index.js +25 -0
- package/dist/ky/index.spec.d.ts +1 -0
- package/dist/ky/index.spec.js +34 -0
- package/package.json +10 -7
- package/dist/index.spec.js +0 -13
- /package/dist/{index.spec.d.ts → fetch/events.spec.d.ts} +0 -0
package/dist/fetch/utils.spec.js
CHANGED
|
@@ -1,282 +1,84 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Headers, Request, Response } from 'undici';
|
|
3
3
|
import { requestIdSymbol } from '../consts.js';
|
|
4
|
-
import { addRequestIdToResponse, parseBody, redactHeaders
|
|
5
|
-
|
|
6
|
-
new Error(`level-${depth}`) :
|
|
7
|
-
new Error(`level-${depth}`, { cause: createMultiLevelError(levels, depth + 1) });
|
|
8
|
-
const walkSerializedCause = (root, steps) => {
|
|
9
|
-
if (steps === 0) {
|
|
10
|
-
return root;
|
|
11
|
-
}
|
|
12
|
-
return walkSerializedCause(root.cause, steps - 1);
|
|
13
|
-
};
|
|
14
|
-
describe('fetch/utils', () => {
|
|
15
|
-
describe('serializeError', () => {
|
|
16
|
-
it('serializes name, message, stack and sets code and cause to undefined when absent', () => {
|
|
17
|
-
const err = new Error('boom');
|
|
18
|
-
expect(serializeError(err)).toEqual({ name: 'Error', message: 'boom', stack: err.stack, code: undefined, cause: undefined });
|
|
19
|
-
});
|
|
20
|
-
it('uses the subclass constructor name', () => {
|
|
21
|
-
const err = new TypeError('bad type');
|
|
22
|
-
expect(serializeError(err).name).toBe('TypeError');
|
|
23
|
-
expect(serializeError(err).message).toBe('bad type');
|
|
24
|
-
});
|
|
25
|
-
it('includes string code when set on the error', () => {
|
|
26
|
-
const err = new Error('e');
|
|
27
|
-
err.code = 'ENOENT';
|
|
28
|
-
expect(serializeError(err).code).toBe('ENOENT');
|
|
29
|
-
});
|
|
30
|
-
it('serializes Error cause as a nested plain object', () => {
|
|
31
|
-
const root = new Error('root');
|
|
32
|
-
const leaf = new TypeError('leaf');
|
|
33
|
-
root.cause = leaf;
|
|
34
|
-
expect(serializeError(root)).toEqual({
|
|
35
|
-
name: 'Error',
|
|
36
|
-
message: 'root',
|
|
37
|
-
stack: root.stack,
|
|
38
|
-
code: undefined,
|
|
39
|
-
cause: {
|
|
40
|
-
name: 'TypeError',
|
|
41
|
-
message: 'leaf',
|
|
42
|
-
stack: leaf.stack,
|
|
43
|
-
code: undefined,
|
|
44
|
-
cause: undefined
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
it('does not recurse into cause when initial depth is already past the limit', () => {
|
|
49
|
-
const inner = new Error('inner');
|
|
50
|
-
const outer = new Error('outer');
|
|
51
|
-
outer.cause = inner;
|
|
52
|
-
expect(serializeError(outer, 6).cause).toBe('<Max recursion depth reached>');
|
|
53
|
-
});
|
|
54
|
-
it('serializes up to five nested Error causes without hitting the sentinel', () => {
|
|
55
|
-
const root = createMultiLevelError(5);
|
|
56
|
-
const innermost = walkSerializedCause(serializeError(root), 4);
|
|
57
|
-
expect(innermost.message).toBe('level-5');
|
|
58
|
-
expect(innermost.cause).toBeUndefined();
|
|
59
|
-
});
|
|
60
|
-
it('replaces cause with the max-depth sentinel on the sixth nested Error', () => {
|
|
61
|
-
const root = createMultiLevelError(6);
|
|
62
|
-
const innermost = walkSerializedCause(serializeError(root), 5);
|
|
63
|
-
expect(innermost.message).toBe('level-6');
|
|
64
|
-
expect(innermost.cause).toBe('<Max recursion depth reached>');
|
|
65
|
-
});
|
|
66
|
-
it('does not expose a seventh error when the chain is longer than the limit', () => {
|
|
67
|
-
const root = createMultiLevelError(7);
|
|
68
|
-
const innermost = walkSerializedCause(serializeError(root), 5);
|
|
69
|
-
expect(innermost.message).toBe('level-6');
|
|
70
|
-
expect(innermost.cause).toBe('<Max recursion depth reached>');
|
|
71
|
-
});
|
|
72
|
-
});
|
|
4
|
+
import { addRequestIdToResponse, parseBody, redactHeaders } from './utils.js';
|
|
5
|
+
describe('instrumented_fetch/utils', () => {
|
|
73
6
|
describe('redactHeaders', () => {
|
|
74
|
-
it('redacts sensitive
|
|
75
|
-
const headers = new Headers([
|
|
76
|
-
['Authorization', 'Bearer token123'],
|
|
77
|
-
['X-API-Key', 'secret-key'],
|
|
78
|
-
['apikey', 'another-secret'],
|
|
79
|
-
['X-Auth-Token', 'auth-token'],
|
|
80
|
-
['Secret-Header', 'top-secret'],
|
|
81
|
-
['Password', 'password123'],
|
|
82
|
-
['Private-Key', 'private-key-data'],
|
|
83
|
-
['Cookie', 'session=abc123'],
|
|
84
|
-
['Content-Type', 'application/json'],
|
|
85
|
-
['User-Agent', 'test-agent']
|
|
86
|
-
]);
|
|
87
|
-
expect(redactHeaders(headers)).toEqual({
|
|
88
|
-
authorization: '[REDACTED]',
|
|
89
|
-
'x-api-key': '[REDACTED]',
|
|
90
|
-
apikey: '[REDACTED]',
|
|
91
|
-
'x-auth-token': '[REDACTED]',
|
|
92
|
-
'secret-header': '[REDACTED]',
|
|
93
|
-
password: '[REDACTED]',
|
|
94
|
-
'private-key': '[REDACTED]',
|
|
95
|
-
cookie: '[REDACTED]',
|
|
96
|
-
'content-type': 'application/json',
|
|
97
|
-
'user-agent': 'test-agent'
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
it('leaves non-sensitive headers unchanged', () => {
|
|
7
|
+
it('redacts sensitive header names while preserving safe values', () => {
|
|
101
8
|
const headers = new Headers({
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
expect(redactHeaders(headers)).toEqual({
|
|
107
|
-
'content-type': 'application/json',
|
|
108
|
-
accept: 'application/json',
|
|
109
|
-
'cache-control': 'no-cache'
|
|
9
|
+
authorization: 'Bearer secret',
|
|
10
|
+
'x-api-key': 'secret',
|
|
11
|
+
cookie: 'session=secret',
|
|
12
|
+
'content-type': 'application/json'
|
|
110
13
|
});
|
|
111
|
-
});
|
|
112
|
-
it('handles empty Headers', () => {
|
|
113
|
-
expect(redactHeaders(new Headers())).toEqual({});
|
|
114
|
-
});
|
|
115
|
-
it('redacts sensitive keys even when values are empty', () => {
|
|
116
|
-
const headers = new Headers([
|
|
117
|
-
['Authorization', ''],
|
|
118
|
-
['Content-Type', 'application/json'],
|
|
119
|
-
['X-API-Key', '']
|
|
120
|
-
]);
|
|
121
14
|
expect(redactHeaders(headers)).toEqual({
|
|
122
15
|
authorization: '[REDACTED]',
|
|
123
|
-
'
|
|
124
|
-
|
|
16
|
+
'x-api-key': '[REDACTED]',
|
|
17
|
+
cookie: '[REDACTED]',
|
|
18
|
+
'content-type': 'application/json'
|
|
125
19
|
});
|
|
126
20
|
});
|
|
127
|
-
it('does not redact
|
|
21
|
+
it('does not redact exempt or substring-only header names', () => {
|
|
128
22
|
const headers = new Headers({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
});
|
|
134
|
-
expect(redactHeaders(headers)).toEqual({
|
|
135
|
-
keyboard: 'qwerty',
|
|
136
|
-
secretary: 'admin',
|
|
137
|
-
tokens: 'abc123',
|
|
138
|
-
'content-length': '123'
|
|
23
|
+
'x-csrf-token': 'csrf-value',
|
|
24
|
+
'public-key-pins': 'pin-value',
|
|
25
|
+
keyboard: 'keyboard-value',
|
|
26
|
+
tokens: 'token-count'
|
|
139
27
|
});
|
|
140
|
-
});
|
|
141
|
-
it('does not redact exempt headers but still redacts any header whose name contains a key segment', () => {
|
|
142
|
-
const headers = new Headers([
|
|
143
|
-
['X-Csrf-Token', 'abc'],
|
|
144
|
-
['Public-Key-Pins', 'pin-sha256=dummy'],
|
|
145
|
-
['ratelimit-tokens-left', '9'],
|
|
146
|
-
['Cache-Key', 'lookup-1'],
|
|
147
|
-
['X-Auth-Token', 'real-secret']
|
|
148
|
-
]);
|
|
149
28
|
expect(redactHeaders(headers)).toEqual({
|
|
150
|
-
'x-csrf-token': '
|
|
151
|
-
'public-key-pins': 'pin-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
'x-auth-token': '[REDACTED]'
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
describe('key suffix pattern /key(?![a-z0-9])/i', () => {
|
|
158
|
-
it.each([
|
|
159
|
-
['x-api-key', 'hyphen before key at end'],
|
|
160
|
-
['apikey', 'single segment ending in key'],
|
|
161
|
-
['X-LicenseKey', 'camel segment ending in Key'],
|
|
162
|
-
['webhook-signing-key', 'key before end'],
|
|
163
|
-
['pre-key-post', 'key surrounded by hyphens'],
|
|
164
|
-
['encryption_key', 'underscore after key'],
|
|
165
|
-
['signing-key-id', 'key in middle of kebab name'],
|
|
166
|
-
['monkey', 'key followed by end of string (substring key)'],
|
|
167
|
-
['turkey-vulture', 'key in first segment before hyphen'],
|
|
168
|
-
['v1__key', 'non-alnum before key']
|
|
169
|
-
])('redacts %s (%s)', headerName => {
|
|
170
|
-
const headers = new Headers([[headerName, 'secret-value']]);
|
|
171
|
-
const out = redactHeaders(headers);
|
|
172
|
-
const canonical = Object.keys(out)[0];
|
|
173
|
-
expect(out[canonical]).toBe('[REDACTED]');
|
|
174
|
-
});
|
|
175
|
-
it.each([
|
|
176
|
-
['keyboard', 'key followed by letter b'],
|
|
177
|
-
['KeyAccountId', 'key followed by letter a'],
|
|
178
|
-
['WhiskeyBar', 'key followed by letter b'],
|
|
179
|
-
['my-keyring', 'key followed by letter r'],
|
|
180
|
-
['Cache-Control', 'no key substring with allowed lookahead']
|
|
181
|
-
])('does not redact %s (%s)', headerName => {
|
|
182
|
-
const headers = new Headers([[headerName, 'visible']]);
|
|
183
|
-
const out = redactHeaders(headers);
|
|
184
|
-
const canonical = Object.keys(out)[0];
|
|
185
|
-
expect(out[canonical]).toBe('visible');
|
|
186
|
-
});
|
|
187
|
-
it('still applies exempt list when the key rule would match (e.g. public-key-pins)', () => {
|
|
188
|
-
const headers = new Headers([['Public-Key-Pins', 'pins-value']]);
|
|
189
|
-
expect(redactHeaders(headers)).toEqual({ 'public-key-pins': 'pins-value' });
|
|
29
|
+
'x-csrf-token': 'csrf-value',
|
|
30
|
+
'public-key-pins': 'pin-value',
|
|
31
|
+
keyboard: 'keyboard-value',
|
|
32
|
+
tokens: 'token-count'
|
|
190
33
|
});
|
|
191
34
|
});
|
|
192
35
|
});
|
|
193
36
|
describe('parseBody', () => {
|
|
194
|
-
it('parses JSON
|
|
37
|
+
it('parses JSON bodies without consuming the original', async () => {
|
|
195
38
|
const response = new Response(JSON.stringify({ ok: true }), {
|
|
196
|
-
headers: { 'content-type': 'application/json' }
|
|
197
|
-
});
|
|
198
|
-
await expect(parseBody(response)).resolves.toEqual({ ok: true });
|
|
199
|
-
});
|
|
200
|
-
it('parses JSON when content-type includes charset', async () => {
|
|
201
|
-
const response = new Response(JSON.stringify([1, 2]), {
|
|
202
39
|
headers: { 'content-type': 'application/json; charset=utf-8' }
|
|
203
40
|
});
|
|
204
|
-
await expect(parseBody(response)).resolves.toEqual(
|
|
41
|
+
await expect(parseBody(response)).resolves.toEqual({ ok: true });
|
|
42
|
+
await expect(response.json()).resolves.toEqual({ ok: true });
|
|
205
43
|
});
|
|
206
|
-
it('returns
|
|
207
|
-
const
|
|
44
|
+
it('returns non-JSON and invalid JSON bodies as text', async () => {
|
|
45
|
+
const textResponse = new Response('plain text', {
|
|
208
46
|
headers: { 'content-type': 'text/plain' }
|
|
209
47
|
});
|
|
210
|
-
|
|
211
|
-
});
|
|
212
|
-
it('uses text branch when content-type is missing', async () => {
|
|
213
|
-
const response = new Response('plain');
|
|
214
|
-
await expect(parseBody(response)).resolves.toBe('plain');
|
|
215
|
-
});
|
|
216
|
-
it('returns empty string for empty body (text)', async () => {
|
|
217
|
-
const response = new Response('', { headers: { 'content-type': 'text/plain' } });
|
|
218
|
-
await expect(parseBody(response)).resolves.toBe('');
|
|
219
|
-
});
|
|
220
|
-
it('returns empty string for empty body even when content-type is application/json', async () => {
|
|
221
|
-
const response = new Response('', { headers: { 'content-type': 'application/json' } });
|
|
222
|
-
await expect(parseBody(response)).resolves.toBe('');
|
|
223
|
-
});
|
|
224
|
-
it('returns raw text when content-type is application/json but the body is not valid JSON', async () => {
|
|
225
|
-
const raw = '{ not json';
|
|
226
|
-
const response = new Response(raw, { headers: { 'content-type': 'application/json' } });
|
|
227
|
-
const result = await parseBody(response);
|
|
228
|
-
expect(result).toBe(raw);
|
|
229
|
-
expect(result).toBeTypeOf('string');
|
|
230
|
-
});
|
|
231
|
-
it('does not consume the original body (clone)', async () => {
|
|
232
|
-
const response = new Response('read-me', { headers: { 'content-type': 'text/plain' } });
|
|
233
|
-
await parseBody(response);
|
|
234
|
-
await expect(response.text()).resolves.toBe('read-me');
|
|
235
|
-
});
|
|
236
|
-
it('parses JSON Request body', async () => {
|
|
237
|
-
const request = new Request('https://ex.com', {
|
|
48
|
+
const invalidJsonRequest = new Request('https://example.com', {
|
|
238
49
|
method: 'POST',
|
|
239
50
|
headers: { 'content-type': 'application/json' },
|
|
240
|
-
body:
|
|
51
|
+
body: '{invalid'
|
|
241
52
|
});
|
|
242
|
-
await expect(parseBody(
|
|
53
|
+
await expect(parseBody(textResponse)).resolves.toBe('plain text');
|
|
54
|
+
await expect(parseBody(invalidJsonRequest)).resolves.toBe('{invalid');
|
|
243
55
|
});
|
|
244
|
-
it('returns
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
method: 'POST',
|
|
248
|
-
headers: { 'content-type': 'application/json' },
|
|
249
|
-
body: raw
|
|
56
|
+
it('returns an empty string for an empty JSON body', async () => {
|
|
57
|
+
const response = new Response('', {
|
|
58
|
+
headers: { 'content-type': 'application/json' }
|
|
250
59
|
});
|
|
251
|
-
await expect(parseBody(
|
|
60
|
+
await expect(parseBody(response)).resolves.toBe('');
|
|
252
61
|
});
|
|
253
62
|
});
|
|
254
63
|
describe('addRequestIdToResponse', () => {
|
|
255
|
-
it('stores
|
|
256
|
-
const response = new Response('ok');
|
|
257
|
-
addRequestIdToResponse(response, 'req-123');
|
|
258
|
-
expect(response[requestIdSymbol]).toBe('req-123');
|
|
259
|
-
});
|
|
260
|
-
it('defines request id as non-enumerable, non-writable and non-configurable', () => {
|
|
64
|
+
it('stores an immutable, non-enumerable request id', () => {
|
|
261
65
|
const response = new Response('ok');
|
|
262
|
-
addRequestIdToResponse(response, '
|
|
263
|
-
|
|
264
|
-
expect(
|
|
265
|
-
|
|
66
|
+
addRequestIdToResponse(response, 'request-1');
|
|
67
|
+
expect(response[requestIdSymbol]).toBe('request-1');
|
|
68
|
+
expect(Object.getOwnPropertyDescriptor(response, requestIdSymbol)).toEqual({
|
|
69
|
+
value: 'request-1',
|
|
266
70
|
enumerable: false,
|
|
267
71
|
configurable: false,
|
|
268
|
-
writable: false
|
|
269
|
-
value: 'req-456'
|
|
72
|
+
writable: false
|
|
270
73
|
});
|
|
271
74
|
});
|
|
272
|
-
it('propagates the request id
|
|
75
|
+
it('propagates the request id through repeated clones', () => {
|
|
273
76
|
const response = new Response('ok');
|
|
274
|
-
addRequestIdToResponse(response, '
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
expect(grandchild[requestIdSymbol]).toBe('req-789');
|
|
77
|
+
addRequestIdToResponse(response, 'request-clone');
|
|
78
|
+
const clone = response.clone();
|
|
79
|
+
const grandchild = clone.clone();
|
|
80
|
+
expect(clone[requestIdSymbol]).toBe('request-clone');
|
|
81
|
+
expect(grandchild[requestIdSymbol]).toBe('request-clone');
|
|
280
82
|
});
|
|
281
83
|
});
|
|
282
84
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Options } from 'ky';
|
|
2
1
|
export type HttpRequestEvent = {
|
|
3
2
|
requestId: string;
|
|
4
3
|
method: string;
|
|
@@ -13,30 +12,9 @@ export type HttpRequestCostEvent = {
|
|
|
13
12
|
url: string;
|
|
14
13
|
total: number;
|
|
15
14
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```ts
|
|
23
|
-
* import { httpClient } from '@outputai/http';
|
|
24
|
-
*
|
|
25
|
-
* const client = httpClient({
|
|
26
|
-
* prefixUrl: 'https://api.example.com',
|
|
27
|
-
* timeout: 30000,
|
|
28
|
-
* retry: { limit: 3 }
|
|
29
|
-
* });
|
|
30
|
-
*
|
|
31
|
-
* const response = await client.get('users/1');
|
|
32
|
-
* const data = await response.json();
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* @param options - The ky options to extend the base client.
|
|
36
|
-
* @returns A ky instance extended with Output.ai tracing hooks.
|
|
37
|
-
*/
|
|
38
|
-
export declare function httpClient(options?: Options): import("ky").KyInstance;
|
|
39
|
-
export { HTTPError, TimeoutError } from 'ky';
|
|
40
|
-
export type { Options as HttpClientOptions } from 'ky';
|
|
41
|
-
export * from './fetch/index.js';
|
|
15
|
+
export { outputFetch } from './fetch/index.js';
|
|
16
|
+
export { createKyClient } from './ky/index.js';
|
|
42
17
|
export { addRequestCost } from './cost.js';
|
|
18
|
+
/** Re-export ky library for convenience. */
|
|
19
|
+
export * as ky from 'ky';
|
|
20
|
+
export * as undici from 'undici';
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Creates a ky client.
|
|
5
|
-
*
|
|
6
|
-
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* import { httpClient } from '@outputai/http';
|
|
11
|
-
*
|
|
12
|
-
* const client = httpClient({
|
|
13
|
-
* prefixUrl: 'https://api.example.com',
|
|
14
|
-
* timeout: 30000,
|
|
15
|
-
* retry: { limit: 3 }
|
|
16
|
-
* });
|
|
17
|
-
*
|
|
18
|
-
* const response = await client.get('users/1');
|
|
19
|
-
* const data = await response.json();
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @param options - The ky options to extend the base client.
|
|
23
|
-
* @returns A ky instance extended with Output.ai tracing hooks.
|
|
24
|
-
*/
|
|
25
|
-
export function httpClient(options = {}) {
|
|
26
|
-
return ky.create({ fetch: customFetch, ...options });
|
|
27
|
-
}
|
|
28
|
-
export { HTTPError, TimeoutError } from 'ky';
|
|
29
|
-
export * from './fetch/index.js';
|
|
1
|
+
export { outputFetch } from './fetch/index.js';
|
|
2
|
+
export { createKyClient } from './ky/index.js';
|
|
30
3
|
export { addRequestCost } from './cost.js';
|
|
4
|
+
/** Re-export ky library for convenience. */
|
|
5
|
+
export * as ky from 'ky';
|
|
6
|
+
export * as undici from 'undici';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { KyInstance, Options } from 'ky';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a ky client.
|
|
4
|
+
*
|
|
5
|
+
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createKyClient } from '@outputai/http';
|
|
10
|
+
*
|
|
11
|
+
* const client = createKyClient({
|
|
12
|
+
* prefix: 'https://api.example.com',
|
|
13
|
+
* timeout: 30000,
|
|
14
|
+
* retry: { limit: 3 }
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const response = await client.get('users/1');
|
|
18
|
+
* const data = await response.json();
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @param options - The ky options to extend the base client.
|
|
22
|
+
* @returns A ky instance extended with Output.ai tracing hooks.
|
|
23
|
+
*/
|
|
24
|
+
export declare const createKyClient: (options?: Options) => KyInstance;
|
package/dist/ky/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ky from 'ky';
|
|
2
|
+
import { outputFetch } from '../fetch/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a ky client.
|
|
5
|
+
*
|
|
6
|
+
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createKyClient } from '@outputai/http';
|
|
11
|
+
*
|
|
12
|
+
* const client = createKyClient({
|
|
13
|
+
* prefix: 'https://api.example.com',
|
|
14
|
+
* timeout: 30000,
|
|
15
|
+
* retry: { limit: 3 }
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* const response = await client.get('users/1');
|
|
19
|
+
* const data = await response.json();
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @param options - The ky options to extend the base client.
|
|
23
|
+
* @returns A ky instance extended with Output.ai tracing hooks.
|
|
24
|
+
*/
|
|
25
|
+
export const createKyClient = (options = {}) => ky.create({ fetch: outputFetch, ...options });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { outputFetch } from '../fetch/index.js';
|
|
3
|
+
const kyMock = vi.hoisted(() => ({
|
|
4
|
+
client: { get: vi.fn() },
|
|
5
|
+
create: vi.fn()
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('ky', () => ({
|
|
8
|
+
default: { create: kyMock.create }
|
|
9
|
+
}));
|
|
10
|
+
import { createKyClient } from './index.js';
|
|
11
|
+
describe('createKyClient', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
kyMock.create.mockReset();
|
|
14
|
+
kyMock.create.mockReturnValue(kyMock.client);
|
|
15
|
+
});
|
|
16
|
+
it('creates a Ky client using outputFetch', () => {
|
|
17
|
+
expect(createKyClient()).toBe(kyMock.client);
|
|
18
|
+
expect(kyMock.create).toHaveBeenCalledWith({ fetch: outputFetch });
|
|
19
|
+
});
|
|
20
|
+
it('forwards Ky options', () => {
|
|
21
|
+
const options = {
|
|
22
|
+
prefix: 'https://example.com',
|
|
23
|
+
timeout: 30_000,
|
|
24
|
+
retry: { limit: 3 }
|
|
25
|
+
};
|
|
26
|
+
createKyClient(options);
|
|
27
|
+
expect(kyMock.create).toHaveBeenCalledWith({ fetch: outputFetch, ...options });
|
|
28
|
+
});
|
|
29
|
+
it('allows callers to override fetch', () => {
|
|
30
|
+
const fetch = vi.fn(async () => new Response());
|
|
31
|
+
createKyClient({ fetch });
|
|
32
|
+
expect(kyMock.create).toHaveBeenCalledWith({ fetch });
|
|
33
|
+
});
|
|
34
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Framework abstraction to make HTTP calls with tracing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,18 +11,21 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"dist"
|
|
13
13
|
],
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"ky": ">=2 <3",
|
|
16
|
+
"undici": ">=8 <9"
|
|
17
|
+
},
|
|
14
18
|
"dependencies": {
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
"@outputai/core": "0.11.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"ky": "2.0.2",
|
|
23
|
+
"undici": "8.9.0"
|
|
18
24
|
},
|
|
19
25
|
"license": "Apache-2.0",
|
|
20
26
|
"publishConfig": {
|
|
21
27
|
"access": "public"
|
|
22
28
|
},
|
|
23
|
-
"imports": {
|
|
24
|
-
"#*": "./dist/*"
|
|
25
|
-
},
|
|
26
29
|
"scripts": {
|
|
27
30
|
"build": "rm -rf ./dist && tsc"
|
|
28
31
|
}
|
package/dist/index.spec.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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();
|
|
12
|
-
});
|
|
13
|
-
});
|
|
File without changes
|