@outputai/http 0.4.1-dev.92bc2fb.0 → 0.4.1-dev.c0b98d8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fetch/index.d.ts +1 -0
- package/dist/fetch/index.js +9 -3
- package/dist/fetch/index.spec.js +7 -2
- package/dist/fetch/logger.d.ts +24 -3
- package/dist/fetch/logger.js +57 -15
- package/dist/fetch/logger.spec.js +77 -7
- package/package.json +2 -2
package/dist/fetch/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ RequestInit };
|
|
|
13
13
|
* Behaves the same as any fetch function except:
|
|
14
14
|
* - Sets a request header called `x-request--trace-id` with a random UUID;
|
|
15
15
|
* - Sends the request, response, error and/or failure to the Trace system;
|
|
16
|
+
* - Emits a `http:request` event on every call (success, http_error, network_error).
|
|
16
17
|
*
|
|
17
18
|
* @see {@link https://fetch.spec.whatwg.org/}
|
|
18
19
|
* @param input - URL string, URL object or Request object (undici's or Node's)
|
package/dist/fetch/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export * as undici from 'undici';
|
|
|
15
15
|
* Behaves the same as any fetch function except:
|
|
16
16
|
* - Sets a request header called `x-request--trace-id` with a random UUID;
|
|
17
17
|
* - Sends the request, response, error and/or failure to the Trace system;
|
|
18
|
+
* - Emits a `http:request` event on every call (success, http_error, network_error).
|
|
18
19
|
*
|
|
19
20
|
* @see {@link https://fetch.spec.whatwg.org/}
|
|
20
21
|
* @param input - URL string, URL object or Request object (undici's or Node's)
|
|
@@ -29,20 +30,25 @@ export const fetch = async (input, init) => {
|
|
|
29
30
|
const requestId = randomUUID();
|
|
30
31
|
headers.set('x-request-trace-id', requestId);
|
|
31
32
|
const request = new undici.Request(base, { headers });
|
|
33
|
+
const method = request.method;
|
|
34
|
+
const url = request.url;
|
|
35
|
+
const startedAt = performance.now();
|
|
32
36
|
await logRequest({ requestId, request });
|
|
33
37
|
try {
|
|
34
38
|
const response = await undici.fetch(request);
|
|
39
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
35
40
|
// This enriches the response of the request id, so it is identifiable later.
|
|
36
41
|
addRequestIdToResponse(response, requestId);
|
|
37
42
|
if (response.status > 399) {
|
|
38
|
-
await logError({ requestId, response });
|
|
43
|
+
await logError({ requestId, response, method, url, durationMs });
|
|
39
44
|
return response;
|
|
40
45
|
}
|
|
41
|
-
await logResponse({ requestId, response });
|
|
46
|
+
await logResponse({ requestId, response, method, url, durationMs });
|
|
42
47
|
return response;
|
|
43
48
|
}
|
|
44
49
|
catch (error) {
|
|
45
|
-
|
|
50
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
51
|
+
logFailure({ requestId, error: error, method, url, durationMs });
|
|
46
52
|
throw error;
|
|
47
53
|
}
|
|
48
54
|
};
|
package/dist/fetch/index.spec.js
CHANGED
|
@@ -62,8 +62,13 @@ describe('fetch/index', () => {
|
|
|
62
62
|
expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('GET');
|
|
63
63
|
expect(loggerMock.logRequest.mock.calls[0][0].request.url).toBe(`${MOCK_ORIGIN}/ok`);
|
|
64
64
|
expect(loggerMock.logResponse).toHaveBeenCalledTimes(1);
|
|
65
|
-
|
|
66
|
-
expect(
|
|
65
|
+
const responseCall = loggerMock.logResponse.mock.calls[0][0];
|
|
66
|
+
expect(responseCall.requestId).toBe(FIXED_REQUEST_ID);
|
|
67
|
+
expect(responseCall.response).toBe(response);
|
|
68
|
+
expect(responseCall.method).toBe('GET');
|
|
69
|
+
expect(responseCall.url).toBe(`${MOCK_ORIGIN}/ok`);
|
|
70
|
+
expect(typeof responseCall.durationMs).toBe('number');
|
|
71
|
+
expect(responseCall.durationMs).toBeGreaterThanOrEqual(0);
|
|
67
72
|
expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledTimes(1);
|
|
68
73
|
expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledWith(response, FIXED_REQUEST_ID);
|
|
69
74
|
expect(loggerMock.logError).not.toHaveBeenCalled();
|
package/dist/fetch/logger.d.ts
CHANGED
|
@@ -12,34 +12,55 @@ export declare const logRequest: ({ requestId, request }: {
|
|
|
12
12
|
}) => Promise<void>;
|
|
13
13
|
/**
|
|
14
14
|
* Sends the trace error event for an http response with error status
|
|
15
|
+
* and emits a `http:request` event with `outcome: 'http_error'`.
|
|
15
16
|
*
|
|
16
17
|
* @param options
|
|
17
18
|
* @param options.requestId - id of the request
|
|
18
19
|
* @param options.response - The HTTP Response object
|
|
20
|
+
* @param options.method - HTTP method of the request
|
|
21
|
+
* @param options.url - URL of the request
|
|
22
|
+
* @param options.durationMs - elapsed time from request issuance to response, in milliseconds
|
|
19
23
|
*/
|
|
20
|
-
export declare const logError: ({ requestId, response }: {
|
|
24
|
+
export declare const logError: ({ requestId, response, method, url, durationMs }: {
|
|
21
25
|
requestId: string;
|
|
22
26
|
response: Response;
|
|
27
|
+
method: string;
|
|
28
|
+
url: string;
|
|
29
|
+
durationMs: number;
|
|
23
30
|
}) => Promise<void>;
|
|
24
31
|
/**
|
|
25
32
|
* Sends the trace end event for an http response
|
|
33
|
+
* and emits a `http:request` event with `outcome: 'success'`.
|
|
26
34
|
*
|
|
27
35
|
* @param {object} options
|
|
28
36
|
* @param options.requestId - id of the request
|
|
29
37
|
* @param {Response} options.response - The HTTP Response object
|
|
38
|
+
* @param options.method - HTTP method of the request
|
|
39
|
+
* @param options.url - URL of the request
|
|
40
|
+
* @param options.durationMs - elapsed time from request issuance to response, in milliseconds
|
|
30
41
|
*/
|
|
31
|
-
export declare const logResponse: ({ requestId, response }: {
|
|
42
|
+
export declare const logResponse: ({ requestId, response, method, url, durationMs }: {
|
|
32
43
|
requestId: string;
|
|
33
44
|
response: Response;
|
|
45
|
+
method: string;
|
|
46
|
+
url: string;
|
|
47
|
+
durationMs: number;
|
|
34
48
|
}) => Promise<void>;
|
|
35
49
|
/**
|
|
36
50
|
* Creates the trace error event for a network/connection failure
|
|
51
|
+
* and emits a `http:request` event with `outcome: 'network_error'`.
|
|
37
52
|
*
|
|
38
53
|
* @param options
|
|
39
54
|
* @param options.requestId - id of the request
|
|
40
55
|
* @param options.error - The error thrown
|
|
56
|
+
* @param options.method - HTTP method of the request
|
|
57
|
+
* @param options.url - URL of the request
|
|
58
|
+
* @param options.durationMs - elapsed time from request issuance to failure, in milliseconds
|
|
41
59
|
*/
|
|
42
|
-
export declare const logFailure: ({ requestId, error }: {
|
|
60
|
+
export declare const logFailure: ({ requestId, error, method, url, durationMs }: {
|
|
43
61
|
requestId: string;
|
|
44
62
|
error: Error;
|
|
63
|
+
method: string;
|
|
64
|
+
url: string;
|
|
65
|
+
durationMs: number;
|
|
45
66
|
}) => void;
|
package/dist/fetch/logger.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Tracing } from '@outputai/core/sdk_activity_integration';
|
|
1
|
+
import { Tracing, emitEvent } from '@outputai/core/sdk_activity_integration';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
3
|
import { parseBody, redactHeaders, serializeError } from './utils.js';
|
|
4
4
|
/**
|
|
@@ -20,38 +20,80 @@ export const logRequest = async ({ requestId, request }) => {
|
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
22
|
* Sends the trace error event for an http response with error status
|
|
23
|
+
* and emits a `http:request` event with `outcome: 'http_error'`.
|
|
23
24
|
*
|
|
24
25
|
* @param options
|
|
25
26
|
* @param options.requestId - id of the request
|
|
26
27
|
* @param options.response - The HTTP Response object
|
|
28
|
+
* @param options.method - HTTP method of the request
|
|
29
|
+
* @param options.url - URL of the request
|
|
30
|
+
* @param options.durationMs - elapsed time from request issuance to response, in milliseconds
|
|
27
31
|
*/
|
|
28
|
-
export const logError = async ({ requestId, response }) =>
|
|
29
|
-
|
|
32
|
+
export const logError = async ({ requestId, response, method, url, durationMs }) => {
|
|
33
|
+
await Tracing.addEventError({
|
|
34
|
+
id: requestId, details: {
|
|
35
|
+
status: response.status,
|
|
36
|
+
statusText: response.statusText,
|
|
37
|
+
headers: redactHeaders(response.headers),
|
|
38
|
+
body: await parseBody(response)
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
emitEvent('http:request', {
|
|
42
|
+
requestId,
|
|
43
|
+
method,
|
|
44
|
+
url,
|
|
30
45
|
status: response.status,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
46
|
+
durationMs,
|
|
47
|
+
outcome: 'http_error'
|
|
48
|
+
});
|
|
49
|
+
};
|
|
36
50
|
/**
|
|
37
51
|
* Sends the trace end event for an http response
|
|
52
|
+
* and emits a `http:request` event with `outcome: 'success'`.
|
|
38
53
|
*
|
|
39
54
|
* @param {object} options
|
|
40
55
|
* @param options.requestId - id of the request
|
|
41
56
|
* @param {Response} options.response - The HTTP Response object
|
|
57
|
+
* @param options.method - HTTP method of the request
|
|
58
|
+
* @param options.url - URL of the request
|
|
59
|
+
* @param options.durationMs - elapsed time from request issuance to response, in milliseconds
|
|
42
60
|
*/
|
|
43
|
-
export const logResponse = async ({ requestId, response }) =>
|
|
44
|
-
|
|
61
|
+
export const logResponse = async ({ requestId, response, method, url, durationMs }) => {
|
|
62
|
+
await Tracing.addEventEnd({
|
|
63
|
+
id: requestId, details: {
|
|
64
|
+
status: response.status,
|
|
65
|
+
statusText: response.statusText,
|
|
66
|
+
...(config.logVerbose && { headers: redactHeaders(response.headers), body: await parseBody(response) })
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
emitEvent('http:request', {
|
|
70
|
+
requestId,
|
|
71
|
+
method,
|
|
72
|
+
url,
|
|
45
73
|
status: response.status,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
}
|
|
74
|
+
durationMs,
|
|
75
|
+
outcome: 'success'
|
|
76
|
+
});
|
|
77
|
+
};
|
|
50
78
|
/**
|
|
51
79
|
* Creates the trace error event for a network/connection failure
|
|
80
|
+
* and emits a `http:request` event with `outcome: 'network_error'`.
|
|
52
81
|
*
|
|
53
82
|
* @param options
|
|
54
83
|
* @param options.requestId - id of the request
|
|
55
84
|
* @param options.error - The error thrown
|
|
85
|
+
* @param options.method - HTTP method of the request
|
|
86
|
+
* @param options.url - URL of the request
|
|
87
|
+
* @param options.durationMs - elapsed time from request issuance to failure, in milliseconds
|
|
56
88
|
*/
|
|
57
|
-
export const logFailure = ({ requestId, error }) =>
|
|
89
|
+
export const logFailure = ({ requestId, error, method, url, durationMs }) => {
|
|
90
|
+
Tracing.addEventError({ id: requestId, details: serializeError(error) });
|
|
91
|
+
emitEvent('http:request', {
|
|
92
|
+
requestId,
|
|
93
|
+
method,
|
|
94
|
+
url,
|
|
95
|
+
status: undefined,
|
|
96
|
+
durationMs,
|
|
97
|
+
outcome: 'network_error'
|
|
98
|
+
});
|
|
99
|
+
};
|
|
@@ -6,10 +6,12 @@ vi.mock('@outputai/core/sdk_activity_integration', () => ({
|
|
|
6
6
|
addEventEnd: vi.fn(),
|
|
7
7
|
addEventError: vi.fn(),
|
|
8
8
|
addEventAttribute: vi.fn()
|
|
9
|
-
}
|
|
9
|
+
},
|
|
10
|
+
emitEvent: vi.fn()
|
|
10
11
|
}));
|
|
11
|
-
import { Tracing } from '@outputai/core/sdk_activity_integration';
|
|
12
|
+
import { Tracing, emitEvent } from '@outputai/core/sdk_activity_integration';
|
|
12
13
|
const tracing = vi.mocked(Tracing, true);
|
|
14
|
+
const emit = vi.mocked(emitEvent, true);
|
|
13
15
|
/** Loads logger with optional verbose tracing env so `config.js` is evaluated fresh. */
|
|
14
16
|
async function logLogger(verbose) {
|
|
15
17
|
vi.resetModules();
|
|
@@ -26,6 +28,7 @@ beforeEach(() => {
|
|
|
26
28
|
tracing.addEventEnd.mockClear();
|
|
27
29
|
tracing.addEventError.mockClear();
|
|
28
30
|
tracing.addEventAttribute.mockClear();
|
|
31
|
+
emit.mockClear();
|
|
29
32
|
});
|
|
30
33
|
describe('fetch/logger', () => {
|
|
31
34
|
describe('logRequest', () => {
|
|
@@ -97,7 +100,9 @@ describe('fetch/logger', () => {
|
|
|
97
100
|
'content-type': 'application/json'
|
|
98
101
|
}
|
|
99
102
|
});
|
|
100
|
-
await logError({
|
|
103
|
+
await logError({
|
|
104
|
+
requestId: 'e1', response, method: 'GET', url: 'https://upstream.test/x', durationMs: 1
|
|
105
|
+
});
|
|
101
106
|
expect(tracing.addEventError).toHaveBeenCalledWith({
|
|
102
107
|
id: 'e1',
|
|
103
108
|
details: {
|
|
@@ -120,7 +125,9 @@ describe('fetch/logger', () => {
|
|
|
120
125
|
statusText: 'Bad Gateway',
|
|
121
126
|
headers: { 'content-type': 'text/plain' }
|
|
122
127
|
});
|
|
123
|
-
await logError({
|
|
128
|
+
await logError({
|
|
129
|
+
requestId: 'e2', response, method: 'GET', url: 'https://upstream.test/y', durationMs: 1
|
|
130
|
+
});
|
|
124
131
|
expect(tracing.addEventError).toHaveBeenCalledWith({
|
|
125
132
|
id: 'e2',
|
|
126
133
|
details: {
|
|
@@ -140,7 +147,9 @@ describe('fetch/logger', () => {
|
|
|
140
147
|
statusText: 'OK',
|
|
141
148
|
headers: { 'content-type': 'application/json', Authorization: 'x' }
|
|
142
149
|
});
|
|
143
|
-
await logResponse({
|
|
150
|
+
await logResponse({
|
|
151
|
+
requestId: 'lr1', response, method: 'GET', url: 'https://x.test/a', durationMs: 1
|
|
152
|
+
});
|
|
144
153
|
expect(tracing.addEventEnd).toHaveBeenCalledWith({
|
|
145
154
|
id: 'lr1',
|
|
146
155
|
details: {
|
|
@@ -159,7 +168,9 @@ describe('fetch/logger', () => {
|
|
|
159
168
|
'Set-Cookie': 'a=b'
|
|
160
169
|
}
|
|
161
170
|
});
|
|
162
|
-
await logResponse({
|
|
171
|
+
await logResponse({
|
|
172
|
+
requestId: 'lr-v', response, method: 'POST', url: 'https://x.test/b', durationMs: 1
|
|
173
|
+
});
|
|
163
174
|
expect(tracing.addEventEnd).toHaveBeenCalledWith({
|
|
164
175
|
id: 'lr-v',
|
|
165
176
|
details: {
|
|
@@ -178,7 +189,7 @@ describe('fetch/logger', () => {
|
|
|
178
189
|
it('forwards serialized error details (including stack) to Tracing.addEventError', async () => {
|
|
179
190
|
const { logFailure } = await logLogger(false);
|
|
180
191
|
const err = new TypeError('network');
|
|
181
|
-
logFailure({ requestId: 'f1', error: err });
|
|
192
|
+
logFailure({ requestId: 'f1', error: err, method: 'GET', url: 'https://example.test/x', durationMs: 12 });
|
|
182
193
|
expect(tracing.addEventError).toHaveBeenCalledWith({
|
|
183
194
|
id: 'f1',
|
|
184
195
|
details: {
|
|
@@ -191,4 +202,63 @@ describe('fetch/logger', () => {
|
|
|
191
202
|
});
|
|
192
203
|
});
|
|
193
204
|
});
|
|
205
|
+
describe('http:request event emission', () => {
|
|
206
|
+
it('emits http:request with outcome=success on logResponse', async () => {
|
|
207
|
+
const { logResponse } = await logLogger(false);
|
|
208
|
+
const response = new Response('', { status: 200 });
|
|
209
|
+
await logResponse({
|
|
210
|
+
requestId: 'r-ok',
|
|
211
|
+
response,
|
|
212
|
+
method: 'GET',
|
|
213
|
+
url: 'https://api.example.com/ok',
|
|
214
|
+
durationMs: 42
|
|
215
|
+
});
|
|
216
|
+
expect(emit).toHaveBeenCalledWith('http:request', {
|
|
217
|
+
requestId: 'r-ok',
|
|
218
|
+
method: 'GET',
|
|
219
|
+
url: 'https://api.example.com/ok',
|
|
220
|
+
status: 200,
|
|
221
|
+
durationMs: 42,
|
|
222
|
+
outcome: 'success'
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
it('emits http:request with outcome=http_error on logError', async () => {
|
|
226
|
+
const { logError } = await logLogger(false);
|
|
227
|
+
const response = new Response('boom', { status: 500 });
|
|
228
|
+
await logError({
|
|
229
|
+
requestId: 'r-err',
|
|
230
|
+
response,
|
|
231
|
+
method: 'POST',
|
|
232
|
+
url: 'https://api.example.com/err',
|
|
233
|
+
durationMs: 15
|
|
234
|
+
});
|
|
235
|
+
expect(emit).toHaveBeenCalledWith('http:request', {
|
|
236
|
+
requestId: 'r-err',
|
|
237
|
+
method: 'POST',
|
|
238
|
+
url: 'https://api.example.com/err',
|
|
239
|
+
status: 500,
|
|
240
|
+
durationMs: 15,
|
|
241
|
+
outcome: 'http_error'
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
it('emits http:request with outcome=network_error on logFailure (status undefined)', async () => {
|
|
245
|
+
const { logFailure } = await logLogger(false);
|
|
246
|
+
const err = new TypeError('network');
|
|
247
|
+
logFailure({
|
|
248
|
+
requestId: 'r-net',
|
|
249
|
+
error: err,
|
|
250
|
+
method: 'GET',
|
|
251
|
+
url: 'https://api.example.com/net',
|
|
252
|
+
durationMs: 9
|
|
253
|
+
});
|
|
254
|
+
expect(emit).toHaveBeenCalledWith('http:request', {
|
|
255
|
+
requestId: 'r-net',
|
|
256
|
+
method: 'GET',
|
|
257
|
+
url: 'https://api.example.com/net',
|
|
258
|
+
status: undefined,
|
|
259
|
+
durationMs: 9,
|
|
260
|
+
outcome: 'network_error'
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
194
264
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/http",
|
|
3
|
-
"version": "0.4.1-dev.
|
|
3
|
+
"version": "0.4.1-dev.c0b98d8.0",
|
|
4
4
|
"description": "Framework abstraction to make HTTP calls with tracing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"ky": "1.14.3",
|
|
13
13
|
"undici": "8.1.0",
|
|
14
|
-
"@outputai/core": "0.4.1-dev.
|
|
14
|
+
"@outputai/core": "0.4.1-dev.c0b98d8.0"
|
|
15
15
|
},
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
17
|
"publishConfig": {
|